diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3303e4bdcbe10c755ed21dae59e9201c243983e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.htaccess +php53.cgi +include/ost-config.php +*.sw[a-z] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8cf7d463e2a4f064a157fa994bb394d3623b9cc --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6ff6cf1b40c0ecc9bb7fcd7e9259b26ae97ea71b --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +osTicket +======== +osTicket is a widely-used open source support ticket system. It seamlessly +integrates inquiries created via email, phone and web-based forms into a +simple easy-to-use multi-user web interface. Manage, organize and archive +all your support requests and responses in one place while providing your +customers with accountability and responsiveness they deserve. + +How osTicket works for you +-------------------------- + 1. Users create tickets via your website, email, or phone + 1. Incoming tickets are saved and assigned to agents + 1. Agents help your users resolve their issues + +osTicket is an attractive alternative to higher-cost and complex customer +support systems; simple, lightweight, reliable, open source, web-based and +easy to setup and use. The best part is, it's completely free. + +Installation +------------ +Download the +[source](./zipball/master) from +the GitHub page. Follow the instructions for installation on the osTicket +[wiki](http://osticket.com/wiki/Installation). + +Help +---- +Visit the [wiki](http://osticket.com/wiki/Home) or the +[forum](http://osticket.com/forums/). And if you'd like professional help +managing your osTicket installation, +[commercial support](http://osticket.com/support/) is available. + +Contributing +------------ +Create your own fork of the project and use +[git-flow](https://github.com/nvie/gitflow) to create a new feature. Once +the feature is published in your fork, send a pull request to begin the +conversation of integrating your new feature into osTicket. diff --git a/api/api.inc.php b/api/api.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..d209860d5c60bff7d55c3032144ea675b4de6767 --- /dev/null +++ b/api/api.inc.php @@ -0,0 +1,85 @@ +<?php +/********************************************************************* + api.inc.php + + File included on every API page...handles security and abuse issues + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +//postfix exit codes see /usr/include/sysexits.h +define('EX_DATAERR', 65); /* data format error */ +define('EX_NOINPUT', 66); /* cannot open input */ +define('EX_UNAVAILABLE', 69); /* service unavailable */ +define('EX_IOERR', 74); /* input/output error */ +define('EX_TEMPFAIL',75); /* temp failure; user is invited to retry */ +define('EX_NOPERM', 77); /* permission denied */ +define('EX_CONFIG', 78); /* configuration error */ + +define('EX_SUCCESS',0); /* success baby */ + +if(!file_exists('../main.inc.php')) exit(EX_CONFIG); +require_once('../main.inc.php'); +if(!defined('INCLUDE_DIR')) exit(EX_CONFIG); + +require_once(INCLUDE_DIR.'class.http.php'); +require_once(INCLUDE_DIR.'class.api.php'); + +define('OSTAPIINC',TRUE); // Define tag that included files can check + +$remotehost=(isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REMOTE_ADDR']))?TRUE:FALSE; +/* API exit helper */ +function api_exit($code,$msg='') { + global $remotehost,$cfg; + + if($code!=EX_SUCCESS) { + //Error occured... + $_SESSION['api']['errors']+=1; + $_SESSION['api']['time']=time(); + Sys::log(LOG_WARNING,"API error - code #$code",$msg); + //echo "API Error:.$msg"; + } + if($remotehost){ + switch($code) { + case EX_SUCCESS: + Http::response(200,$code,'text/plain'); + break; + case EX_UNAVAILABLE: + Http::response(405,$code,'text/plain'); + break; + case EX_NOPERM: + Http::response(403,$code,'text/plain'); + break; + case EX_DATAERR: + case EX_NOINPUT: + default: + Http::response(416,$code,'text/plain'); + } + } + exit($code); +} + +//Remote hosts need authorization. +if($remotehost) { + + $ip=$_SERVER['REMOTE_ADDR']; + $key=$_SERVER['HTTP_USER_AGENT']; //pulling all tricks. + //Upto 10 consecutive errors allowed...before a 5 minute timeout. + //One more error during timeout and timeout starts a new clock + if($_SESSION['api']['errors']>10 && (time()-$_SESSION['api']['time'])<=5*60) { // timeout! + api_exit(EX_NOPERM,"Remote host [$ip] in timeout - error #".$_SESSION['api']['errors']); + } + //Check API key & ip + if(!Validator::is_ip($ip) || !Api::validate($key,$ip)) { + api_exit(EX_NOPERM,'Unknown remote host ['.$ip.'] or invalid API key ['.$key.']'); + } + //At this point we know the remote host/IP is allowed. + $_SESSION['api']['errors']=0; //clear errors for the session. +} +?> diff --git a/api/cron.php b/api/cron.php new file mode 100644 index 0000000000000000000000000000000000000000..86de02cdb6e32a4f3f61e5fc87a2567133694f82 --- /dev/null +++ b/api/cron.php @@ -0,0 +1,21 @@ +<?php +/********************************************************************* + cron.php + + File to handle cron job calls (local and remote). + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. +require('api.inc.php'); +require_once(INCLUDE_DIR.'class.cron.php'); +Cron::run(); +Sys::log(LOG_DEBUG,'Cron Job','External cron job executed ['.$_SERVER['REMOTE_ADDR'].']'); +?> diff --git a/api/http.php b/api/http.php new file mode 100644 index 0000000000000000000000000000000000000000..646df6798fb8a6f0cdfcc4425114f6dfff71ee91 --- /dev/null +++ b/api/http.php @@ -0,0 +1,27 @@ +<?php +/********************************************************************* + api.php + + Controller for the osTicket API + + Jared Hancock + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +chdir('..'); +include "main.inc.php"; + +# Include the main api urls +require_once INCLUDE_DIR."class.dispatcher.php"; +$dispatcher = Dispatcher::include_urls("urls.conf.php"); + +# Call the respective function +$dispatcher->resolve($_SERVER['PATH_INFO']); + +?> diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000000000000000000000000000000000000..f9518a67ebc4a50b4c9b59f7920221c245a6b0dc --- /dev/null +++ b/api/index.php @@ -0,0 +1,3 @@ +<?php +header('Location: ../'); +?> diff --git a/api/pipe.php b/api/pipe.php new file mode 100644 index 0000000000000000000000000000000000000000..b4e5e5ad5c247cb93de0dad36281651d2d2a365d --- /dev/null +++ b/api/pipe.php @@ -0,0 +1,132 @@ +#!/usr/bin/php -q +<?php +/********************************************************************* + pipe.php + + Converts piped emails to ticket. Both local and remote! + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. +ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments. +require('api.inc.php'); +require_once(INCLUDE_DIR.'class.mailparse.php'); +require_once(INCLUDE_DIR.'class.email.php'); + +//Make sure piping is enabled! +if(!$cfg->enableEmailPiping()) + api_exit(EX_UNAVAILABLE,'Email piping not enabled - check MTA settings.'); +//Get the input +$data=isset($_SERVER['HTTP_HOST'])?file_get_contents('php://input'):file_get_contents('php://stdin'); +if(empty($data)){ + api_exit(EX_NOINPUT,'No data'); +} + +//Parse the email. +$parser= new Mail_Parse($data); +if(!$parser->decode()){ //Decode...returns false on decoding errors + api_exit(EX_DATAERR,'Email parse failed ['.$parser->getError()."]\n\n".$data); +} + + + +//Check from address. make sure it is not a banned address. +$fromlist = $parser->getFromAddressList(); +//Check for parsing errors on FROM address. +if(!$fromlist || PEAR::isError($fromlist)){ + api_exit(EX_DATAERR,'Invalid FROM address ['.$fromlist?$fromlist->getMessage():''."]\n\n".$data); +} + +$from=$fromlist[0]; //Default. +foreach($fromlist as $fromobj){ + if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) + continue; + $from=$fromobj; + break; +} + +//TO Address:Try to figure out the email associated with the message. +$tolist = $parser->getToAddressList(); +foreach ($tolist as $toaddr){ + if(($emailId=Email::getIdByEmail($toaddr->mailbox.'@'.$toaddr->host))){ + //We've found target email. + break; + } +} +if(!$emailId && ($cclist=$parser->getCcAddressList())) { + foreach ($cclist as $ccaddr){ + if(($emailId=Email::getIdByEmail($ccaddr->mailbox.'@'.$ccaddr->host))){ + break; + } + } +} +//TODO: Options to reject emails without a matching To address in db? May be it was Bcc? Current Policy: If you pipe, we accept policy + +require_once(INCLUDE_DIR.'class.ticket.php'); //We now need this bad boy! + +$var=array(); +$deptId=0; +$name=trim($from->personal,'"'); +if($from->comment && $from->comment[0]) + $name.=' ('.$from->comment[0].')'; +$subj=utf8_encode($parser->getSubject()); +if(!($body=Format::stripEmptyLines($parser->getBody())) && $subj) + $body=$subj; + +$var['mid']=$parser->getMessageId(); +$var['email']=$from->mailbox.'@'.$from->host; +$var['name']=$name?utf8_encode($name):$var['email']; +$var['emailId']=$emailId?$emailId:$cfg->getDefaultEmailId(); +$var['subject']=$subj?$subj:'[No Subject]'; +$var['message']=utf8_encode(Format::stripEmptyLines($body)); +$var['header']=$parser->getHeader(); +$var['pri']=$cfg->useEmailPriority()?$parser->getPriority():0; + +$ticket=null; +if(preg_match ("[[#][0-9]{1,10}]",$var['subject'],$regs)) { + $extid=trim(preg_replace("/[^0-9]/", "", $regs[0])); + $ticket= new Ticket(Ticket::getIdByExtId($extid)); + //Allow mismatched emails?? For now hell NO. + if(!is_object($ticket) || strcasecmp($ticket->getEmail(),$var['email'])) + $ticket=null; +} +$errors=array(); +$msgid=0; +if(!$ticket){ //New tickets... + # Apply filters against the new ticket + $ef = new EmailFilter($var); $ef->apply($var); + $ticket=Ticket::create($var,$errors,'email'); + if(!is_object($ticket) || $errors){ + api_exit(EX_DATAERR,'Ticket create Failed '.implode("\n",$errors)."\n\n"); + } + $msgid=$ticket->getLastMsgId(); +}else{ + $message=$var['message']; + //Strip quoted reply...TODO: figure out how mail clients do it without special tag.. + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()) && strpos($var['message'],$tag)) + list($message)=split($tag,$var['message']); + //post message....postMessage does the cleanup. + if(!($msgid=$ticket->postMessage($message,'Email',$var['mid'],$var['header']))) { + api_exit(EX_DATAERR,"Unable to post message \n\n $message\n"); + } +} +//Ticket created...save attachments if enabled. +if($cfg->allowEmailAttachments()) { + if($attachments=$parser->getAttachments()){ + //print_r($attachments); + foreach($attachments as $k=>$attachment){ + if($attachment['filename'] && $cfg->canUploadFileType($attachment['filename'])) { + $ticket->saveAttachment($attachment['filename'],$attachment['body'],$msgid,'M'); + } + } + } +} +api_exit(EX_SUCCESS); +?> diff --git a/api/urls.conf.php b/api/urls.conf.php new file mode 100644 index 0000000000000000000000000000000000000000..b6a9555e72565e4d945b78ab30f17be0c63bfd84 --- /dev/null +++ b/api/urls.conf.php @@ -0,0 +1,11 @@ +<?php + +# What about patterns("api/ticket.php:TicketController", ...) since if the +# class is given as the prefix, it isn't defined in more than one file. This +# would allow for speficying imports only if an item is defined in a +# different class (with the array("class", "method") syntax) +return patterns("api.ticket.php:TicketController", + url_post("^/tickets\.(?P<format>xml|json)$", "create") +); + +?> diff --git a/assets/default/css/print.css b/assets/default/css/print.css new file mode 100644 index 0000000000000000000000000000000000000000..9f1c25746ca0486d2524e24ea850b1d2ffcd9e6c --- /dev/null +++ b/assets/default/css/print.css @@ -0,0 +1,29 @@ +#header, #nav, #meta, #footer, #reply, #pagination, .reload, .refresh, form, .thread, hr, #kbAttachments, .back { + display: none; +} + +th { + text-align: left; +} + +a { + color: #000; + text-decoration: none; +} + +caption { + text-align: left; + padding-bottom: 10px; + font-weight: bold; +} + +.message, .response { + border-bottom: 1px solid #000; + margin-bottom: 20px; + padding-bottom: 10px; +} +.message th, .response th { + font-size: 12pt; + font-weight: bold; + padding-bottom: 5px; +} diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css new file mode 100644 index 0000000000000000000000000000000000000000..dde40189e88227edaa3f1c18e4aec6b7e1bb67db --- /dev/null +++ b/assets/default/css/theme.css @@ -0,0 +1,712 @@ +/* Based on Normalize.css - with tags we won't use removed. */ +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + margin: 0; + font-size: 13px; + line-height: 1.231; + padding: 0; +} + +body, input, select, textarea { + font-family: sans-serif; + color: #000; +} + +b, strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +small { + font-size: 85%; +} + +ul, ol { + margin: 1em 0; + padding: 0 0 0 30px; +} + +img { + border: 0; + vertical-align: middle; +} + +form { + margin: 0; +} + +fieldset { + border: 0; + margin: 0; + padding: 0; +} + +label { + cursor: pointer; +} + +input, select, textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; +} + +input { + line-height: normal; + *overflow: visible; +} + +table input { + *overflow: auto; +} + +input[type="button"], input[type="reset"], input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="checkbox"], input[type="radio"] { + box-sizing: border-box; +} + +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} + +textarea { + overflow: auto; + vertical-align: top; + resize: vertical; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td { + vertical-align: top; +} + +h1, h2, h3, h4, h5, h6, form, fieldset { + margin: 0; + padding: 0; +} + +/* Typography */ +a { + color: #0072bc; + text-decoration: none; +} + +h1 { + color: #00AEEF; + font-weight: normal; + font-size: 20px; +} + +h3 { + font-size: 16px; +} + +h2 { + font-size: 16px; + color: #999; +} + +/* Helpers */ +.centered { text-align: center;} + +.clear { clear:both; height: 1px; visibility: none;} + +.hidden { display: none;} + +.faded { color:#666;} + +/* Pagination */ +#pagination { + border: 0; + margin: 0 0 40px 0; + padding: 0; +} +#pagination li { + border: 0; + margin: 0; + padding: 0; + font-size: 11px; + list-style: none; + display: inline; +} +#pagination a { + margin-right: 2px; + display: block; + float: left; + padding: 3px 6px; + text-decoration: none; +} +#pagination a:hover { + color: #ff0084; +} +#pagination .previousOff, #pagination .nextOff { + color: #666; + display: block; + float: left; + font-weight: bold; + padding: 3px 4px; +} +#pagination .next a, #pagination .previous a { + font-weight: bold; +} +#pagination .active { + color: #000; + font-weight: bold; + margin-right: 2px; + display: block; + float: left; + padding: 3px 6px; + text-decoration: none; +} + +/* Alerts & Notices */ + +#msg_notice { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; } + +#msg_warning { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #f26522; background: url('../images/icons/alert.png') 10px 50% no-repeat #ffffdd; } + +#msg_error { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #a00; background: url('../images/icons/error.png') 10px 50% no-repeat #fff0f0; } + + +.warning { + background: #ffc; + font-style: italic; +} +.warning strong { + text-transform: uppercase; + color: #a00; + font-style: normal; +} + +.error { + color:#f00; +} + +.error input { + border:1px solid #f00;} + +/* Main layout */ +body { + background: url('../images/page_bg.png') top left repeat-x #c8c8c8; +} + +#container { + background: #fff; + width: 840px; + margin: 0 auto; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); +} + +#header { + position: relative; + height: 71px; + padding: 0 20px; +} +#header #logo { + width: 220px; + height: 71px; + float: left; +} + +#header p { + width: 400px; + text-align: right; + margin: 0; + padding: 10px 0; + float: right; +} + +#nav { + margin: 0 20px; + padding: 2px 10px; + height: 20px; + background: url('../images/nav_bg.png') top left repeat-x; + border-top: 1px solid #aaa; +} +#nav li { + margin: 0; + padding: 0; + list-style: none; + display: inline; +} +#nav li a { + display: block; + width: auto; + float: left; + height: 20px; + line-height: 20px; + text-align: center; + padding: 0 10px 0 32px; + margin-left: 10px; + color: #333; + border-radius: 20px; + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + background-position: 10px 50%; + background-repeat: no-repeat; +} +#nav li a.active, #nav li a:hover { + background-color: #dbefff; + color: #000; +} +#nav li a:hover { + background-color: #ededed; + color: #0054a6; +} +#nav li a.home { + background-image: url('../images/icons/home.png'); +} +#nav li a.kb { + background-image: url('../images/icons/kb.png'); +} +#nav li a.new { + background-image: url('../images/icons/new.png'); +} +#nav li a.status { + background-image: url('../images/icons/status.png'); +} +#nav li a.tickets { + background-image: url('../images/icons/tix.png'); +} + +#content { + padding: 20px 0; + margin: 0 20px; + background: url('../images/content_bg.png') top left repeat-x; +} + +#cnbg { + padding: 5px 0; + margin: 0 20px; + height: auto !important; + height: 350px; + min-height: 350px; + background: none; +} + + +#footer { + text-align: center; + font-size: 11px; + color: #333; +} +#footer a { + color: #333; +} + +#footer p { + margin: 10px 0 0 0; +} +#footer #poweredBy { + display: block; + width: 126px; + height: 23px; + outline: none; + text-indent: -9999px; + margin: 0 auto; + background: url('../images/poweredby.png?1319571688') top left no-repeat; +} + +/* Landing page */ +#landing_page #new_ticket { + margin-top: 40px; + width: 295px; + padding-left: 75px; + float: left; + background: url('../images/new_ticket_icon.png?1319577021') top left no-repeat; +} +#landing_page #new_ticket input[type=submit] { + background: url('../images/open_ticket_btn.png?1319566422') top left no-repeat; +} +#landing_page #check_status { + margin-top: 40px; + width: 295px; + padding-left: 75px; + float: right; + background: url('../images/check_status_icon.png?1319577072') top left no-repeat; +} +#landing_page #check_status input[type=submit] { + background: url('../images/check_status_btn.png?1319571066') top left no-repeat; +} +#landing_page form div { + margin-bottom: 20px; + height: 60px; + min-height: 60px; +} +#landing_page form input[type=submit] { + display: block; + width: 192px; + height: 38px; + border: none; + margin: 0; + padding: 0; + text-indent: -9999px; +} + +#faq { + clear: both; + margin: 0; + padding: 5px; +} +#faq ol { + font-size: 15px; + margin-left: 0; + padding-left: 0; +} +#faq ol li { + list-style: none; + margin: 0 0 10px 0; + color: #999; +} +#faq ol li a { + display: block; + height: 16px; + line-height: 16px; + padding-left: 24px; + background: url('../images/icons/page.png?1319579499') 0 50% no-repeat; +} + +/* Knowledgebase */ +#kb { + margin: 2px 0; + padding: 5px; + overflow: hidden; +} + +#kb > li { + margin: 0 0 5px 0; + padding: 10px; + width: auto; + float: left; + clear: both; + list-style: none; +} + +#kb > li h4 { + padding-bottom:3px; + margin-bottom:3px; +} + +#kb > li h4 span { + color:#666; +} + +#kb > li h4 a { + font-size: 14px; + padding-left: 24px; + background: url('../images/icons/page.png?1319579499') 0 50% no-repeat; +} + + +#breadcrumbs { + color: #333; + margin-bottom: 15px; +} +#breadcrumbs a { + color: #555; +} + +/* New Ticket & Log In Forms */ +#ticketForm div, #clientLogin div { + clear: both; + padding: 3px 0; + overflow: hidden; +} +#ticketForm div label, #clientLogin div label { + display: block; + width: 140px; + float: left; +} +#ticketForm div input, #ticketForm div textarea, #clientLogin div input, #clientLogin div textarea { + width: auto; + border: 1px solid #aaa; + background: #fff; + margin-right: 10px; + display: block; + float: left; +} +#ticketForm div select, #clientLogin div select { + display: block; + float: left; +} +#ticketForm div textarea, #clientLogin div textarea { + width: 600px; +} +#ticketForm div em, #clientLogin div em { + color: #777; +} +#ticketForm div .captcha, #clientLogin div .captcha { + width: 88px; + height: 31px; + background: #000; + display: block; + float: left; + margin-right: 20px; +} +#ticketForm div label.inline, #clientLogin div label.inline { + width: auto; + padding: 0 10px; +} +#ticketForm div label.required, #clientLogin div label.required { + font-weight: bold; +} +#ticketForm div.captchaRow, #clientLogin div.captchaRow { + line-height: 31px; +} +#ticketForm div.captchaRow input, #clientLogin div.captchaRow input { + position: relative; + top: 6px; +} +#ticketForm div.error input, #clientLogin div.error input { + border: 1px solid #a00; +} +#ticketForm div.error label, #clientLogin div.error label { + color: #a00; +} +#ticketForm p, #clientLogin p { + clear: both; + text-align: center; +} + +#clientLogin { + width: 400px; + margin-top: 20px; + padding: 10px 100px 10px 10px; + border: 1px solid #ccc; + background: url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6; +} +#clientLogin strong { + font-size: 11px; + color: #d00; + display: block; + padding-left: 140px; +} +#clientLogin #email { + width: 250px; + margin-right: 0; +} +#clientLogin #ticketno { + width: 120px; + margin-right: 0; +} + +/* Ticket List */ +.Icon { + width: auto; + padding-left: 20px; + background-position: left center; + background-repeat: no-repeat; + color: #006699; + text-decoration: none; +} + +a.Icon:hover { + text-decoration: underline; +} + +.Icon.Ticket { + background: url('../images/icons/ticket.gif?1319654018') 0 0 no-repeat; +} + +.Icon.webTicket { + background: url('../images/icons/ticket_source_web.gif?1319654283') 0 0 no-repeat; +} + +.Icon.emailTicket { + background: url('../images/icons/ticket_source_email.gif?1319654484') 0 0 no-repeat; +} + +.Icon.phoneTicket { + background: url('../images/icons/ticket_source_phone.gif?1319654401') 0 0 no-repeat; +} + +.Icon.otherTicket { + background: url('../images/icons/ticket_source_other.gif?1319654433') 0 0 no-repeat; +} + +.Icon.attachment { + background-image: url('../images/icons/attachment.gif?1319556657'); +} + +.Icon.file { + background-image: url('../images/icons/attachment.gif?1319556657'); +} + +.Icon.refresh { + background-image: url('../images/icons/refresh.gif?1319556657'); +} + +.Icon.thread { + font-weight: bold; + font-size: 1em; + background-image: url('../images/icons/thread.gif?1319556657'); +} + +#ticketTable { + border: 1px solid #aaa; + border-left: none; + border-bottom: none; +} +#ticketTable caption { + padding: 5px; + text-align: left; + color: #000; + background: #ddd; + border: 1px solid #aaa; + border-bottom: none; + font-weight: bold; +} +#ticketTable th { + height: 24px; + line-height: 24px; + background: #e1f2ff; + border: 1px solid #aaa; + border-right: none; + border-top: none; +} +#ticketTable th a { + color: #000; +} +#ticketTable td { + padding: 2px; + border: 1px solid #aaa; + border-right: none; + border-top: none; +} +#ticketTable tr.alt td { + background: #f9f9f9; +} + +#ticketSearchForm { + display: inline-block; + float: left; + padding: 0 0 5px 0; +} + +a.refresh { + display: block; + width: auto; + float: right; + height: 20px; + line-height: 20px; + text-align: center; + padding: 0 10px 0 28px; + border: 1px solid #aaa; + margin-left: 10px; + color: #333; + background-position: 5px 50%; + background-repeat: no-repeat; + background-image: url('../images/icons/refresh.png?1319653435'); +} + +.infoTable { + background: #F4FAFF; +} +.infoTable th { + text-align: left; +} + +#ticketThread table { + margin-top: 10px; + border: 1px solid #aaa; + border-bottom: 2px solid #aaa; +} +#ticketThread table th { + text-align: left; + border-bottom: 1px solid #aaa; + font-size: 11pt; + padding: 5px; +} +#ticketThread table td { + padding: 5px; +} +#ticketThread .message th { + background: #d8efff; +} +#ticketThread .response th { + background: #ddd; +} +#ticketThread .info { + padding: 5px; + background: #f9f9f9; + border-top: 1px solid #ddd; + height: 16px; + line-height: 16px; +} +#ticketThread .info a { + display: inline-block; + margin: 5px 20px 5px 0; + padding-left: 24px; + height: 16px; + line-height: 16px; + background-position: 0 50%; + background-repeat: no-repeat; +} +#ticketThread .info .pdf { + background-image: url('../images/filetypes/pdf.png?1319636863'); +} + +#reply { + margin-top: 20px; + padding: 10px 5px; + background: #f9f9f9; + border: 1px solid #ccc; +} +#reply h2 { + margin-bottom: 10px; +} +#reply table { + width: 800px; +} +#reply table td { + vertical-align: top; +} +#reply textarea { + width: 628px !important; +} +#reply input[type=text], #reply #response_options textarea { + border: 1px solid #aaa; + background: #fff; +} +#reply .attachments .uploads div { + display: inline-block; + padding-right: 20px; +} +#reply .file { + display: inline-block; + padding-left: 20px; + margin-right: 20px; + background: url('../images/icons/file.gif') 0 50% no-repeat; +} diff --git a/assets/default/images/check_status_btn.png b/assets/default/images/check_status_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..b0856af9da7e655e4f9d55f990dba0e6fa3621ef Binary files /dev/null and b/assets/default/images/check_status_btn.png differ diff --git a/assets/default/images/check_status_icon.png b/assets/default/images/check_status_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf89d50a3f887b11e97ece8a8e3a85e64d090b56 Binary files /dev/null and b/assets/default/images/check_status_icon.png differ diff --git a/assets/default/images/content_bg.png b/assets/default/images/content_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..9ecfda98f95871b4130f4f19fdc20385e98ff97f Binary files /dev/null and b/assets/default/images/content_bg.png differ diff --git a/assets/default/images/filetypes/pdf.png b/assets/default/images/filetypes/pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8e68ac0fbe6b0f7cd5ed924eae9896fd62f98a Binary files /dev/null and b/assets/default/images/filetypes/pdf.png differ diff --git a/assets/default/images/icons/alert.png b/assets/default/images/icons/alert.png new file mode 100755 index 0000000000000000000000000000000000000000..8892a55a4ef40387dd9a210f581d3c6f779a1114 Binary files /dev/null and b/assets/default/images/icons/alert.png differ diff --git a/assets/default/images/icons/attachment.gif b/assets/default/images/icons/attachment.gif new file mode 100644 index 0000000000000000000000000000000000000000..4400e61e9812a3b2a070c89fc6fce7489c104e17 Binary files /dev/null and b/assets/default/images/icons/attachment.gif differ diff --git a/assets/default/images/icons/error.png b/assets/default/images/icons/error.png new file mode 100755 index 0000000000000000000000000000000000000000..a11afcfd53f37bbe6881198792fb21da1607cfc3 Binary files /dev/null and b/assets/default/images/icons/error.png differ diff --git a/assets/default/images/icons/home.png b/assets/default/images/icons/home.png new file mode 100644 index 0000000000000000000000000000000000000000..7625feaa1497acb36923517e50a91277a4ec996c Binary files /dev/null and b/assets/default/images/icons/home.png differ diff --git a/assets/default/images/icons/kb.png b/assets/default/images/icons/kb.png new file mode 100644 index 0000000000000000000000000000000000000000..c373e787b534e78f64992c62482a7f105f652882 Binary files /dev/null and b/assets/default/images/icons/kb.png differ diff --git a/assets/default/images/icons/lock.png b/assets/default/images/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8e7c722395e1762d611532052d76820e41e652 Binary files /dev/null and b/assets/default/images/icons/lock.png differ diff --git a/assets/default/images/icons/new.png b/assets/default/images/icons/new.png new file mode 100644 index 0000000000000000000000000000000000000000..75564839ee6446896f11f6506ed30528c53bc9be Binary files /dev/null and b/assets/default/images/icons/new.png differ diff --git a/assets/default/images/icons/ok.png b/assets/default/images/icons/ok.png new file mode 100755 index 0000000000000000000000000000000000000000..7a5d21fdad11fe220217e8da11df8b0af947cae1 Binary files /dev/null and b/assets/default/images/icons/ok.png differ diff --git a/assets/default/images/icons/page.png b/assets/default/images/icons/page.png new file mode 100644 index 0000000000000000000000000000000000000000..c24bf87795c2b4e5715aa39af8b6cc22a3331d4f Binary files /dev/null and b/assets/default/images/icons/page.png differ diff --git a/assets/default/images/icons/refresh.gif b/assets/default/images/icons/refresh.gif new file mode 100644 index 0000000000000000000000000000000000000000..8268958a19e016741fffb8309b1174e548f5ce19 Binary files /dev/null and b/assets/default/images/icons/refresh.gif differ diff --git a/assets/default/images/icons/refresh.png b/assets/default/images/icons/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6dd226f9bccc58db5f5dae91191502cb11fcfa Binary files /dev/null and b/assets/default/images/icons/refresh.png differ diff --git a/assets/default/images/icons/status.png b/assets/default/images/icons/status.png new file mode 100644 index 0000000000000000000000000000000000000000..4c211878b8060c7372480a33db96a43697ac10ee Binary files /dev/null and b/assets/default/images/icons/status.png differ diff --git a/assets/default/images/icons/thread.gif b/assets/default/images/icons/thread.gif new file mode 100644 index 0000000000000000000000000000000000000000..bffd6b0b3cf5ce0cadfc38683ee7fb3fa0a5c82a Binary files /dev/null and b/assets/default/images/icons/thread.gif differ diff --git a/assets/default/images/icons/ticket.gif b/assets/default/images/icons/ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..2b4b91d5a3e57ff2d9c2fcdff260b7c801273b8c Binary files /dev/null and b/assets/default/images/icons/ticket.gif differ diff --git a/assets/default/images/icons/ticket_source_email.gif b/assets/default/images/icons/ticket_source_email.gif new file mode 100644 index 0000000000000000000000000000000000000000..20eff712f985a32d8f3a91c687db138dc709d29d Binary files /dev/null and b/assets/default/images/icons/ticket_source_email.gif differ diff --git a/assets/default/images/icons/ticket_source_other.gif b/assets/default/images/icons/ticket_source_other.gif new file mode 100644 index 0000000000000000000000000000000000000000..2b4b91d5a3e57ff2d9c2fcdff260b7c801273b8c Binary files /dev/null and b/assets/default/images/icons/ticket_source_other.gif differ diff --git a/assets/default/images/icons/ticket_source_phone.gif b/assets/default/images/icons/ticket_source_phone.gif new file mode 100644 index 0000000000000000000000000000000000000000..44bf451c91a9689dfd29c077349b4576b4fae954 Binary files /dev/null and b/assets/default/images/icons/ticket_source_phone.gif differ diff --git a/assets/default/images/icons/ticket_source_web.gif b/assets/default/images/icons/ticket_source_web.gif new file mode 100644 index 0000000000000000000000000000000000000000..25ec9db8fd6efcd0a25aef6f514d6140c8db08ec Binary files /dev/null and b/assets/default/images/icons/ticket_source_web.gif differ diff --git a/assets/default/images/icons/tix.png b/assets/default/images/icons/tix.png new file mode 100644 index 0000000000000000000000000000000000000000..d66c07f6264e3555d021ddbe20f1fb3318ee7331 Binary files /dev/null and b/assets/default/images/icons/tix.png differ diff --git a/assets/default/images/icons/tix_closed.png b/assets/default/images/icons/tix_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..f63f93fa3949a8b70b07e44824bd748dd5523f4d Binary files /dev/null and b/assets/default/images/icons/tix_closed.png differ diff --git a/assets/default/images/lock.png b/assets/default/images/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..d619684d3e137b36e5c17f4ec1e49a956e46f0b5 Binary files /dev/null and b/assets/default/images/lock.png differ diff --git a/assets/default/images/logo.png b/assets/default/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3b021740c7850b14d840fdf43bf790a06dc475fb Binary files /dev/null and b/assets/default/images/logo.png differ diff --git a/assets/default/images/nav_bg.png b/assets/default/images/nav_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..db3196a03416d35641b5af5c46208ef71d94cf2b Binary files /dev/null and b/assets/default/images/nav_bg.png differ diff --git a/assets/default/images/new_ticket_icon.png b/assets/default/images/new_ticket_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08bed66b49c0997aa092636cb587be51a29f0d96 Binary files /dev/null and b/assets/default/images/new_ticket_icon.png differ diff --git a/assets/default/images/open_ticket_btn.png b/assets/default/images/open_ticket_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..9cd429e6470dd927b9715bf046cab30f10aa1313 Binary files /dev/null and b/assets/default/images/open_ticket_btn.png differ diff --git a/assets/default/images/page_bg.png b/assets/default/images/page_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..49445a45e6a4960779f79d73776dd60c3bfb4574 Binary files /dev/null and b/assets/default/images/page_bg.png differ diff --git a/assets/default/images/poweredby.png b/assets/default/images/poweredby.png new file mode 100644 index 0000000000000000000000000000000000000000..18cfcdd4050cde428500032d52db74609ae9e10f Binary files /dev/null and b/assets/default/images/poweredby.png differ diff --git a/assets/default/images/support.png b/assets/default/images/support.png new file mode 100644 index 0000000000000000000000000000000000000000..53ae09f63ac0a0d16fcc16ea3e9eaa8246ac46e2 Binary files /dev/null and b/assets/default/images/support.png differ diff --git a/client.inc.php b/client.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..5461bd2f75246f57b710f74312eb45b9c4cfba9a --- /dev/null +++ b/client.inc.php @@ -0,0 +1,65 @@ +<?php +/********************************************************************* + client.inc.php + + File included on every client page + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); + +$thisdir=str_replace('\\\\', '/', realpath(dirname(__FILE__))).'/'; +if(!file_exists($thisdir.'main.inc.php')) die('Fatal Error.'); + +require_once($thisdir.'main.inc.php'); + +if(!defined('INCLUDE_DIR')) die('Fatal error'); + +/*Some more include defines specific to client only */ +define('CLIENTINC_DIR',INCLUDE_DIR.'client/'); +define('OSTCLIENTINC',TRUE); + +define('ASSETS_PATH',ROOT_PATH.'assets/default/'); + + +//Check the status of the HelpDesk. +if(!is_object($cfg) || !$cfg->getId() || $cfg->isHelpDeskOffline()) { + include('./offline.php'); + exit; +} + +//Forced upgrade? Version mismatch. +if(defined('THIS_VERSION') && strcasecmp($cfg->getVersion(),THIS_VERSION)) { + die('System is offline for an upgrade.'); + exit; +} + + + +/* include what is needed on client stuff */ +require_once(INCLUDE_DIR.'class.client.php'); +require_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.dept.php'); + +//clear some vars +$errors=array(); +$msg=''; +$thisclient=null; +//Make sure the user is valid..before doing anything else. +if($_SESSION['_client']['userID'] && $_SESSION['_client']['key']) + $thisclient = new ClientSession($_SESSION['_client']['userID'],$_SESSION['_client']['key']); + +//is the user logged in? +if($thisclient && $thisclient->getId() && $thisclient->isValid()){ + $thisclient->refreshSession(); +} + +$nav = new UserNav($thisclient, 'home'); +?> diff --git a/images/bg.gif b/images/bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..e20f31775822d9b0a7a01ddaf38cb1f48a60e6f2 Binary files /dev/null and b/images/bg.gif differ diff --git a/images/cal.gif b/images/cal.gif new file mode 100644 index 0000000000000000000000000000000000000000..8526cf5d19a915aa8073cf344873c4505491970d Binary files /dev/null and b/images/cal.gif differ diff --git a/images/captcha/bubbles.png b/images/captcha/bubbles.png new file mode 100644 index 0000000000000000000000000000000000000000..229fe90befb4f9b87acf9d6842ae8c6757e0cc85 Binary files /dev/null and b/images/captcha/bubbles.png differ diff --git a/images/captcha/cottoncandy.png b/images/captcha/cottoncandy.png new file mode 100644 index 0000000000000000000000000000000000000000..a029c420fa38f9efd86bc2b6c95182d68e1946cf Binary files /dev/null and b/images/captcha/cottoncandy.png differ diff --git a/images/captcha/crackle.png b/images/captcha/crackle.png new file mode 100644 index 0000000000000000000000000000000000000000..ca80322d4cfd52cf11384bb5900e50f0ea242ee1 Binary files /dev/null and b/images/captcha/crackle.png differ diff --git a/images/captcha/grass.png b/images/captcha/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..c31029e44d81a5d2b3ea554c390fa7744722ef83 Binary files /dev/null and b/images/captcha/grass.png differ diff --git a/images/captcha/lines.png b/images/captcha/lines.png new file mode 100644 index 0000000000000000000000000000000000000000..2e517f2aa9be3316b039e50513c7bb51a4022cd0 Binary files /dev/null and b/images/captcha/lines.png differ diff --git a/images/captcha/ripple.png b/images/captcha/ripple.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e768f1354bf1d23fe4d867784a667925f68b20 Binary files /dev/null and b/images/captcha/ripple.png differ diff --git a/images/captcha/sand.png b/images/captcha/sand.png new file mode 100644 index 0000000000000000000000000000000000000000..afd9f4428494dc3e8cb0cbe8dcc1c48c2ebe3e19 Binary files /dev/null and b/images/captcha/sand.png differ diff --git a/images/captcha/silk.png b/images/captcha/silk.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1f5cab06fe633e1ba070f7a30c6700e75cefb0 Binary files /dev/null and b/images/captcha/silk.png differ diff --git a/images/captcha/snakeskin.png b/images/captcha/snakeskin.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7d520e64c44ae9f6f15dd21f98bbda829f7889 Binary files /dev/null and b/images/captcha/snakeskin.png differ diff --git a/images/captcha/whirlpool.png b/images/captcha/whirlpool.png new file mode 100644 index 0000000000000000000000000000000000000000..aa29fd1b793d575d1d67a828a89916d0ab45cec0 Binary files /dev/null and b/images/captcha/whirlpool.png differ diff --git a/images/fibres.png b/images/fibres.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad3ac27f71bdf17f637ffae3d86d36e24359764 Binary files /dev/null and b/images/fibres.png differ diff --git a/images/home.gif b/images/home.gif new file mode 100644 index 0000000000000000000000000000000000000000..b25c0781dd0b8f66d4a9271b662061b54d7a9c4d Binary files /dev/null and b/images/home.gif differ diff --git a/images/icons/attachment.gif b/images/icons/attachment.gif new file mode 100644 index 0000000000000000000000000000000000000000..4400e61e9812a3b2a070c89fc6fce7489c104e17 Binary files /dev/null and b/images/icons/attachment.gif differ diff --git a/images/icons/refresh.gif b/images/icons/refresh.gif new file mode 100644 index 0000000000000000000000000000000000000000..8268958a19e016741fffb8309b1174e548f5ce19 Binary files /dev/null and b/images/icons/refresh.gif differ diff --git a/images/icons/thread.gif b/images/icons/thread.gif new file mode 100644 index 0000000000000000000000000000000000000000..bffd6b0b3cf5ce0cadfc38683ee7fb3fa0a5c82a Binary files /dev/null and b/images/icons/thread.gif differ diff --git a/images/icons/ticket.gif b/images/icons/ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..4304ea7955091c46d9fe570faefc643773a5c1de Binary files /dev/null and b/images/icons/ticket.gif differ diff --git a/images/icons/ticket_source_email.gif b/images/icons/ticket_source_email.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b57605d47b7fc2e4729b5a6602bdf4768ad749f Binary files /dev/null and b/images/icons/ticket_source_email.gif differ diff --git a/images/icons/ticket_source_other.gif b/images/icons/ticket_source_other.gif new file mode 100644 index 0000000000000000000000000000000000000000..4304ea7955091c46d9fe570faefc643773a5c1de Binary files /dev/null and b/images/icons/ticket_source_other.gif differ diff --git a/images/icons/ticket_source_phone.gif b/images/icons/ticket_source_phone.gif new file mode 100644 index 0000000000000000000000000000000000000000..b9aa8ed58a2993eab69a1c83fba78bb627e9004f Binary files /dev/null and b/images/icons/ticket_source_phone.gif differ diff --git a/images/icons/ticket_source_web.gif b/images/icons/ticket_source_web.gif new file mode 100644 index 0000000000000000000000000000000000000000..1b7a5b909075fb879056d15468e407df21e0507d Binary files /dev/null and b/images/icons/ticket_source_web.gif differ diff --git a/images/lipsum.png b/images/lipsum.png new file mode 100644 index 0000000000000000000000000000000000000000..feb6a95fba027cb3d54883fc64ade1294c74bd09 Binary files /dev/null and b/images/lipsum.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..256344139522f7f8bc65b36e1ef0890e0fa3cf52 Binary files /dev/null and b/images/logo.png differ diff --git a/images/logo2.jpg b/images/logo2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16bc12ed36e707ace9d2d9e5443cc6d3a895711b Binary files /dev/null and b/images/logo2.jpg differ diff --git a/images/logout.gif b/images/logout.gif new file mode 100644 index 0000000000000000000000000000000000000000..6dd774f09e48292379250ac0f1be17ccdb30baec Binary files /dev/null and b/images/logout.gif differ diff --git a/images/my_tickets.gif b/images/my_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee2d25bbedd01ba5a64e9a66f5fc72b7b876f10c Binary files /dev/null and b/images/my_tickets.gif differ diff --git a/images/new_ticket.gif b/images/new_ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..32d9636e71fa848c3b7d0f0d8eb559b78465ef22 Binary files /dev/null and b/images/new_ticket.gif differ diff --git a/images/new_ticket_icon.jpg b/images/new_ticket_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..855eef0644245185b7b2f700d35d6a953aa65cd1 Binary files /dev/null and b/images/new_ticket_icon.jpg differ diff --git a/images/poweredby.jpg b/images/poweredby.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c98eb7d679b09975599711643cfcd3549c0540f6 Binary files /dev/null and b/images/poweredby.jpg differ diff --git a/images/rainbow.png b/images/rainbow.png new file mode 100644 index 0000000000000000000000000000000000000000..c08f52edb522e5ec42c237994622f3b8520b428b Binary files /dev/null and b/images/rainbow.png differ diff --git a/images/refresh_btn.gif b/images/refresh_btn.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a33b22d9a9b026391e38a8bbb0d6834eb789a93 Binary files /dev/null and b/images/refresh_btn.gif differ diff --git a/images/ticket_status.gif b/images/ticket_status.gif new file mode 100644 index 0000000000000000000000000000000000000000..0775549996b70c79816e455e6850d1abf0e376ab Binary files /dev/null and b/images/ticket_status.gif differ diff --git a/images/ticket_status_icon.jpg b/images/ticket_status_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bf27b1f904418c311f05b27f118a55622be51b47 Binary files /dev/null and b/images/ticket_status_icon.jpg differ diff --git a/images/verticalbar.jpg b/images/verticalbar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2678913d2cdb9b8ec861e71396fcb92a552ff13a Binary files /dev/null and b/images/verticalbar.jpg differ diff --git a/images/view_closed_btn.gif b/images/view_closed_btn.gif new file mode 100644 index 0000000000000000000000000000000000000000..6cd8f080a9007a85a2c0f08a6133f54a0253d6e8 Binary files /dev/null and b/images/view_closed_btn.gif differ diff --git a/images/view_open_btn.gif b/images/view_open_btn.gif new file mode 100644 index 0000000000000000000000000000000000000000..8ed6be9ce8159b20abfb0c9ea69b5235a4903b33 Binary files /dev/null and b/images/view_open_btn.gif differ diff --git a/include/JSON.php b/include/JSON.php new file mode 100644 index 0000000000000000000000000000000000000000..e75eba65b8ed62d0c8648fe4bd322177a7ffab46 --- /dev/null +++ b/include/JSON.php @@ -0,0 +1,805 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * Converts to and from JSON format. + * + * JSON (JavaScript Object Notation) is a lightweight data-interchange + * format. It is easy for humans to read and write. It is easy for machines + * to parse and generate. It is based on a subset of the JavaScript + * Programming Language, Standard ECMA-262 3rd Edition - December 1999. + * This feature can also be found in Python. JSON is a text format that is + * completely language independent but uses conventions that are familiar + * to programmers of the C-family of languages, including C, C++, C#, Java, + * JavaScript, Perl, TCL, and many others. These properties make JSON an + * ideal data-interchange language. + * + * This package provides a simple encoder and decoder for JSON notation. It + * is intended for use with client-side Javascript applications that make + * use of HTTPRequest to perform server communication functions - data can + * be encoded into JSON notation for use in a client-side javascript, or + * decoded from incoming Javascript requests. JSON format is native to + * Javascript, and can be directly eval()'ed with no further parsing + * overhead + * + * All strings should be in ASCII or UTF-8 format! + * + * LICENSE: Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: Redistributions of source code must retain the + * above copyright notice, this list of conditions and the following + * disclaimer. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * @category + * @package Services_JSON + * @author Michal Migurski <mike-json@teczno.com> + * @author Matt Knapp <mdknapp[at]gmail[dot]com> + * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> + * @copyright 2005 Michal Migurski + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_SLICE', 1); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_STR', 2); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_ARR', 3); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_OBJ', 4); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_CMT', 5); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * <code> + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * </code> + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var) + { + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + } + + // treat it like a regular array + $elements = array_map(array($this, 'encode'), $var); + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = get_object_vars($var); + + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode(strval($name)) . ':' . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { + // found a quote, we're in a string, and it's not escaped + // we know that it's not escaped becase there is _not_ an + // odd number of backslashes at the end of the string so far + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> diff --git a/include/PasswordHash.php b/include/PasswordHash.php new file mode 100644 index 0000000000000000000000000000000000000000..a30f41137c1334522168deb428869d60c7ead7b2 --- /dev/null +++ b/include/PasswordHash.php @@ -0,0 +1,253 @@ +<?php +# +# Portable PHP password hashing framework. +# +# Version 0.3 / genuine. +# +# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in +# the public domain. Revised in subsequent years, still public domain. +# +# There's absolutely no warranty. +# +# The homepage URL for this framework is: +# +# http://www.openwall.com/phpass/ +# +# Please be sure to update the Version line if you edit this file in any way. +# It is suggested that you leave the main version number intact, but indicate +# your project name (after the slash) and add your own revision information. +# +# Please do not change the "private" password hashing method implemented in +# here, thereby making your hashes incompatible. However, if you must, please +# change the hash type identifier (the "$P$") to something different. +# +# Obviously, since this code is in the public domain, the above are not +# requirements (there can be none), but merely suggestions. +# +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime(); + if (function_exists('getmypid')) + $this->random_state .= getmypid(); + } + + function get_random_bytes($count) + { + $output = ''; + if (is_readable('/dev/urandom') && + ($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= + pack('H*', md5($this->random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) == $output) + $output = '*1'; + + $id = substr($setting, 0, 3); + # We use "$P$", phpBB3 uses "$H$" for the same thing + if ($id != '$P$' && $id != '$H$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) != 8) + return $output; + + # We're kind of forced to use MD5 here since it's the only + # cryptographic primitive available in all versions of PHP + # currently in use. To implement our own low-level crypto + # in PHP would result in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + if (PHP_VERSION >= '5') { + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + } else { + $hash = pack('H*', md5($salt . $password)); + do { + $hash = pack('H*', md5($hash . $password)); + } while (--$count); + } + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_extended($input) + { + $count_log2 = min($this->iteration_count_log2 + 8, 24); + # This should be odd to not reveal weak DES keys, and the + # maximum valid value is (2**24 - 1) which is odd anyway. + $count = (1 << $count_log2) - 1; + + $output = '_'; + $output .= $this->itoa64[$count & 0x3f]; + $output .= $this->itoa64[($count >> 6) & 0x3f]; + $output .= $this->itoa64[($count >> 12) & 0x3f]; + $output .= $this->itoa64[($count >> 18) & 0x3f]; + + $output .= $this->encode64($input, 3); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) == 60) + return $hash; + } + + if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) { + if (strlen($random) < 3) + $random = $this->get_random_bytes(3); + $hash = + crypt($password, $this->gensalt_extended($random)); + if (strlen($hash) == 20) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) == 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] == '*') + $hash = crypt($password, $stored_hash); + + return $hash == $stored_hash; + } +} + +?> diff --git a/include/ajax.config.php b/include/ajax.config.php new file mode 100644 index 0000000000000000000000000000000000000000..7dcfd9972717e9c8cd5cb14a217b0e3b361dd9ee --- /dev/null +++ b/include/ajax.config.php @@ -0,0 +1,32 @@ +<?php +/********************************************************************* + ajax.content.php + + AJAX interface for content fetching...allowed methods. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +if(!defined('INCLUDE_DIR')) die('!'); + +class ConfigAjaxAPI extends AjaxController { + + //config info UI might need. + function ui() { + global $thisstaff, $cfg; + + $config=array('ticket_lock_time'=>($cfg->getLockTime()*3600), + 'max_attachments'=>$cfg->getMaxFileUploads(), + 'max_file_size'=>$cfg->getMaxFileSize()); + + return $this->json_encode($config); + } +} +?> diff --git a/include/ajax.content.php b/include/ajax.content.php new file mode 100644 index 0000000000000000000000000000000000000000..e77f45d23014d2176b24500d31e26c1ab263133f --- /dev/null +++ b/include/ajax.content.php @@ -0,0 +1,83 @@ +<?php +/********************************************************************* + ajax.content.php + + AJAX interface for content fetching...allowed methods. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +if(!defined('INCLUDE_DIR')) die('!'); + +class ContentAjaxAPI extends AjaxController { + + function log($id) { + + if($id && ($log=Log::lookup($id))) { + $content=sprintf('<div style="width:500px;"> <strong>%s</strong><br><p>%s</p> + <hr><strong>Log Date:</strong> <em>%s</em> <strong>IP Address:</strong> <em>%s</em></div>', + $log->getTitle(), + Format::display(str_replace(',',', ',$log->getText())), + Format::db_daydatetime($log->getCreateDate()), + $log->getIP()); + }else { + $content='<div style="width:295px;"> <strong>Error:</strong>Unknown or invalid log ID</div>'; + } + + return $content; + } + + function ticket_variables() { + + $content='<div style="width:600px;"> + <h2>Ticket Variables</h2> + Please note that non-base variables depends on the context of use. + <br> + <table width="100%" border="0" cellspacing=1 cellpadding=2> + <tr><td width="50%" valign="top"><b>Base Variables</b></td><td><b>Other Variables</b></td></tr> + <tr> + <td width="50%" valign="top"> + <table width="100%" border="0" cellspacing=1 cellpadding=1> + <tr><td width="100">%id</td><td>Ticket ID (internal ID)</td></tr> + <tr><td>%ticket</td><td>Ticket number (external ID)</td></tr> + <tr><td>%email</td><td>Email address</td></tr> + <tr><td>%name</td><td>Full name</td></tr> + <tr><td>%subject</td><td>Subject</td></tr> + <tr><td>%topic</td><td>Help topic (web only)</td></tr> + <tr><td>%phone</td><td>Phone number | ext</td></tr> + <tr><td>%status</td><td>Status</td></tr> + <tr><td>%priority</td><td>Priority</td></tr> + <tr><td>%dept</td><td>Department</td></tr> + <tr><td>%assigned</td><td>Assigned staff or team (if any)</td></tr> + <tr><td>%createdate</td><td>Date created</td></tr> + <tr><td>%duedate</td><td>Due date</td></tr> + <tr><td>%closedate</td><td>Date closed</td></tr> + </table> + </td> + <td valign="top"> + <table width="100%" border="0" cellspacing=1 cellpadding=1> + <tr><td width="100">%message</td><td>Message (incoming)</td></tr> + <tr><td>%response</td><td>Response (outgoing)</td></tr> + <tr><td>%note</td><td>Internal/transfer note</td></tr> + <tr><td>%staff</td><td>Staff\'s name (alert/notices)</td></tr> + <tr><td>%assignee</td><td>Assigned staff</td></tr> + <tr><td>%assigner</td><td>Staff assigning the ticket</td></tr> + <tr><td>%url</td><td>osTicket\'s base url (FQDN)</td></tr> + + </table> + </td> + </tr> + </table> + </div>'; + + return $content; + } +} +?> diff --git a/include/ajax.kbase.php b/include/ajax.kbase.php new file mode 100644 index 0000000000000000000000000000000000000000..b51d8781dff46f94409c7b797e59973862d222a9 --- /dev/null +++ b/include/ajax.kbase.php @@ -0,0 +1,83 @@ +<?php +/********************************************************************* + ajax.kbase.php + + AJAX interface for knowledge base related...allowed methods. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +if(!defined('INCLUDE_DIR')) die('!'); + + +class KbaseAjaxAPI extends AjaxController { + + function cannedResp($id, $format='') { + global $thisstaff, $_GET; + + include_once(INCLUDE_DIR.'class.canned.php'); + + if(!$id || !($canned=Canned::lookup($id)) || !$canned->isEnabled()) + Http::response(404, 'No such premade reply'); + + //Load ticket. + if($_GET['tid']) { + include_once(INCLUDE_DIR.'class.ticket.php'); + $ticket = Ticket::lookup($_GET['tid']); + } + + switch($format) { + case 'json': + $resp['id'] = $canned->getId(); + $resp['ticket'] = $canned->getTitle(); + $resp['response'] = $ticket?$ticket->replaceTemplateVars($canned->getResponse()):$canned->getResponse(); + $resp['files'] = $canned->getAttachments(); + + + $response = $this->json_encode($resp); + break; + case 'txt': + default: + $response =$ticket?$ticket->replaceTemplateVars($canned->getResponse()):$canned->getResponse(); + } + + + return $response; + } + + function faq($id, $format='html') { + global $thisstaff; //XXX: user ajax->getThisStaff() + include_once(INCLUDE_DIR.'class.faq.php'); + + if(!($faq=FAQ::lookup($id))) + return null; + + //TODO: $fag->getJSON() for json format. + $resp = sprintf( + '<div style="width:650px;"> + <strong>%s</strong><p>%s</p> + <div class="faded">Last updated %s</div> + <hr> + <a href="faq.php?id=%d">View</a> | <a href="faq.php?id=%d">Attachments (%s)</a>', + $faq->getQuestion(), + Format::safe_html($faq->getAnswer()), + Format::db_daydatetime($faq->getUpdateDate()), + $faq->getId(), + $faq->getId(), + $faq->getNumAttachments()); + if($thisstaff && $thisstaff->canManageFAQ()) { + $resp.=sprintf(' | <a href="faq.php?id=%d&a=edit">Edit</a>',$faq->getId()); + + } + $resp.='</div>'; + + return $resp; + } +} +?> diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php new file mode 100644 index 0000000000000000000000000000000000000000..2eff8924b84c1d909d8c35da3ea171d4ebf2619a --- /dev/null +++ b/include/ajax.tickets.php @@ -0,0 +1,164 @@ +<?php +/********************************************************************* + ajax.tickets.php + + AJAX interface for tickets + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +if(!defined('INCLUDE_DIR')) die('403'); + +include_once(INCLUDE_DIR.'class.ticket.php'); + +class TicketsAjaxAPI extends AjaxController { + + function search() { + + $limit = isset($_GET['limit']) ? (int) $_GET['limit']:25; + $items=array(); + $ticketid=false; + if(isset($_GET['id'])){ + $WHERE=' WHERE ticketID LIKE \''.db_input($_GET['id'], false).'%\''; + $ticketid=true; + }elseif(isset($_GET['email'])){ + $WHERE=' WHERE email LIKE \''.db_input(strtolower($_GET['email']), false).'%\''; + }else{ + Http::response(400, "id or email argument is required"); + } + $sql='SELECT DISTINCT ticketID,email,name FROM '.TICKET_TABLE.' '.$WHERE.' ORDER BY created LIMIT '.$limit; + $res=db_query($sql); + if($res && db_num_rows($res)){ + while(list($id,$email,$name)=db_fetch_row($res)) { + $info=($ticketid)?$email:$id; + $id=($ticketid)?$id:$email; + # TODO: Return 'name' from email address if 'email' argument + # specified? + $items[] = array('id'=>$id, 'value'=>$id, 'info'=>$info, + 'name'=>$name); + } + } + return $this->encode(array('results'=>$items)); + } + + function acquireLock($tid) { + global $cfg,$thisstaff; + + if(!$tid or !is_numeric($tid) or !$thisstaff or !$cfg) + return 0; + + $ticket = Ticket::lookup($tid); + + if(!$ticket || !$ticket->checkStaffAccess($thisstaff)) + return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>'Lock denied!')); + + //is the ticket already locked? + if($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) { + /*Note: Ticket->acquireLock does the same logic...but we need it here since we need to know who owns the lock up front*/ + //Ticket is locked by someone else.?? + if($lock->getStaffId()!=$thisstaff->getId()) + return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>'Unable to acquire lock.')); + + //Ticket already locked by staff...try renewing it. + $lock->renew(); //New clock baby! + + return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); + } + + //Ticket is not locked or the lock is expired...try locking it... + if($lock=$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime())) //Set the lock. + return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); + + //unable to obtain the lock..for some really weired reason! + //Client should watch for possible loop on retries. Max attempts? + return $this->json_encode(array('id'=>0, 'retry'=>true)); + } + + function renewLock($tid, $id) { + global $thisstaff; + + if(!$id or !is_numeric($id) or !$thisstaff) + return $this->json_encode(array('id'=>0, 'retry'=>true)); + + $lock= TicketLock::lookup($id); + if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired + return self::acquireLock($tid); //acquire the lock + + if($lock->getStaffId()!=$thisstaff->getId()) //user doesn't own the lock anymore??? sorry...try to next time. + return $this->json_encode(array('id'=>0, 'retry'=>false)); //Give up... + + //Renew the lock. + $lock->renew(); //Failure here is not an issue since the lock is not expired yet.. client need to check time! + + return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); + } + + function releaseLock($tid, $id=0) { + global $thisstaff; + + if($id && is_numeric($id)){ //Lock Id provided! + + $lock = TicketLock::lookup($id, $tid); + //Already gone? + if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired + return 1; + + //make sure the user actually owns the lock before releasing it. + return ($lock->getStaffId()==$thisstaff->getId() && $lock->release())?1:0; + + }elseif($tid){ //release all the locks the user owns on the ticket. + return TicketLock::removeStaffLocks($thisstaff->getId(),$tid)?1:0; + } + + return 0; + } + + function previewTicket ($tid) { + + global $thisstaff; + + + $ticket = new Ticket($tid); + + $resp = sprintf( + '<div style="width:500px;"> + <strong>Ticket #%d Preview</strong><br>INFO HERE!!', + $ticket->getExtId()); + + $options[]=array('action'=>'Thread ('.$ticket->getThreadCount().')','url'=>"tickets.php?id=$tid"); + if($ticket->getNumNotes()) + $options[]=array('action'=>'Notes ('.$ticket->getNumNotes().')','url'=>"tickets.php?id=$tid#notes"); + + if($ticket->isOpen()) + $options[]=array('action'=>'Reply','url'=>"tickets.php?id=$tid#reply"); + + if($thisstaff->canAssignTickets()) + $options[]=array('action'=>($ticket->isAssigned()?'Reassign':'Assign'),'url'=>"tickets.php?id=$tid#assign"); + + if($thisstaff->canTransferTickets()) + $options[]=array('action'=>'Transfer','url'=>"tickets.php?id=$tid#transfer"); + + $options[]=array('action'=>'Post Note','url'=>"tickets.php?id=$tid#note"); + + if($options) { + $resp.='<ul class="tip_menu">'; + foreach($options as $option) { + $resp.=sprintf('<li><a href="%s">%s</a></li>', + $option['url'],$option['action']); + } + $resp.='</ul>'; + } + + $resp.='</div>'; + + return $resp; + } +} +?> diff --git a/include/api.ticket.php b/include/api.ticket.php new file mode 100644 index 0000000000000000000000000000000000000000..d767e2128ba2ee4f60abcc44f2fc10b2f4af1a23 --- /dev/null +++ b/include/api.ticket.php @@ -0,0 +1,71 @@ +<?php + +include_once "include/class.api.php"; +include_once "include/class.ticket.php"; + +class TicketController extends ApiController { + + # Supported arguments -- anything else is an error. These items will be + # inspected _after_ the fixup() method of the ApiXxxDataParser classes + # so that all supported input formats should be supported + function getRequestStructure($format) { + $supported = array( + "alert", "autorespond", "source", + "name", "email", "subject", "phone", "phone_ext", + "attachments" => array("*" => + array("name", "type", "data", "encoding") + ), + "message", "ip" + ); + if ($format == "xml") return array("ticket" => $supported); + else return $supported; + } + + function create($format) { + $this->requireApiKey(); + + # Parse request body + $data = $this->getRequest($format); + if ($format == "xml") $data = $data["ticket"]; + + # Pull off some meta-data + $alert = $data['alert'] ? $data['alert'] : true; + $autorespond = $data['autorespond'] ? $data['autorespond'] : true; + $source = $data['source'] ? $data['source'] : 'API'; + + # TODO: Handle attachment encoding (base64) + foreach ($data["attachments"] as $filename=>&$info) { + if ($info["encoding"] == "base64") { + # XXX: May fail on large inputs. See + # http://us.php.net/manual/en/function.base64-decode.php#105512 + if (!($info["data"] = base64_decode($info["data"], true))) + Http::response(400, sprintf( + "%s: Poorly encoded base64 data", + $filename)); + } + $info['size'] = strlen($info['data']); + } + + # Create the ticket with the data (attempt to anyway) + $errors = array(); + $ticket = Ticket::create($data, $errors, $source, $autorespond, + $alert); + + # Return errors (?) + if (count($errors)) { + Http::response(400, "Unable to create new ticket: validation errors:\n" + . Format::array_implode(": ", "\n", $errors)); + } elseif (!$ticket) { + Http::response(500, "Unable to create new ticket: unknown error"); + } + + # Save attachment(s) + foreach ($data["attachments"] as &$info) + $ticket->saveAttachment($info, $ticket->getLastMsgId(), "M"); + + # All done. Return HTTP/201 --> Created + Http::response(201, $ticket->getExtId()); + } +} + +?> diff --git a/include/class.ajax.php b/include/class.ajax.php new file mode 100644 index 0000000000000000000000000000000000000000..742f224fb1e9064ec9abcb73bd12d8e8725531ed --- /dev/null +++ b/include/class.ajax.php @@ -0,0 +1,53 @@ +<?php +/********************************************************************* + class.ajax.php + + AjaxController class that is an extension of the ApiController class. It + will be used to provide functionality common to all Ajax API calls + + Jared Hancock + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once (INCLUDE_DIR.'class.api.php'); + +/** + * AjaxController Class + * A simple extension of the ApiController class that will assist in + * providing functionality common to all Ajax call controllers. Any Ajax + * call controller should inherit from this class in order to maintain + * consistency. + */ +class AjaxController extends ApiController { + function AjaxController() { + # Security checks first + # --> It is assumed that all AJAX calls will require a login. And + # for now, since client logins are not yet supported, a staff + # login will be required for AJAX calls. + $this->staffOnly(); + } + function staffOnly() { + global $thisstaff; + if(!$thisstaff || !$thisstaff->isValid()) { + Http::response(401,'Access Denied. IP '.$_SERVER['REMOTE_ADDR']); + } + } + /** + * Convert a PHP array into a JSON-encoded string + */ + function json_encode($what) { + require_once (INCLUDE_DIR.'class.json.php'); + $encoder = new JsonDataEncoder(); + return $encoder->encode($what); + } + + function encode($what) { + return $this->json_encode($what); + } +} diff --git a/include/class.api.php b/include/class.api.php new file mode 100644 index 0000000000000000000000000000000000000000..1bd25463484a4e2f01a1d72050dde4e92d89d967 --- /dev/null +++ b/include/class.api.php @@ -0,0 +1,284 @@ +<?php +/********************************************************************* + class.api.php + + API + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class API { + + var $id; + + var $info; + + function API($id){ + $this->id=0; + $this->load($id); + } + + function load($id) { + + $sql='SELECT * FROM '.API_KEY_TABLE.' WHERE id='.db_input($id); + if(($res=db_query($sql)) && db_num_rows($res)) { + $info=db_fetch_array($res); + $this->id=$info['id']; + $this->info=$info; + return true; + } + return false; + } + + function reload() { + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getKey(){ + return $this->info['apikey']; + } + + function getIPAddr(){ + return $this->info['ipaddr']; + } + + function getNotes(){ + return $this->info['notes']; + } + + function isActive(){ + return ($this->info['isactive']); + } + + function update($vars,&$errors){ + if(API::save($this->getId(),$vars,$errors)){ + $this->reload(); + return true; + } + + return false; + } + + function delete(){ + $sql='DELETE FROM '.API_KEY_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1'; + return (db_query($sql) && ($num=db_affected_rows())); + } + + /** Static functions **/ + function add($vars,&$errors){ + return API::save(0,$vars,$errors); + } + + function validate($key,$ip){ + + $sql='SELECT id FROM '.API_KEY_TABLE.' WHERE ipaddr='.db_input($ip).' AND apikey='.db_input($key); + return (($res=db_query($sql)) && db_num_rows($res)); + } + + function getKeyByIPAddr($ip){ + + $sql='SELECT apikey FROM '.API_KEY_TABLE.' WHERE ipaddr='.db_input($ip); + if(($res=db_query($sql)) && db_num_rows($res)) + list($key)=db_fetch_row($res); + + return $key; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($k= new API($id)) && $k->getId()==$id)?$k:null; + } + + function save($id,$vars,&$errors){ + + if(!$id) { + if(!$vars['ipaddr'] || !Validator::is_ip($vars['ipaddr'])) + $errors['ipaddr']='Valid IP required'; + elseif(API::getKeyByIPAddr($vars['ipaddr'])) + $errors['ipaddr']='API key for the IP already exists'; + } + + if($errors) return false; + + + $sql=' updated=NOW() '. + ',isactive='.db_input($vars['isactive']). + ',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.API_KEY_TABLE.' SET '.$sql.' WHERE id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update API key. Internal error occurred'; + }else{ + $sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql.',created=NOW() '. + ',ipaddr='.db_input($vars['ipaddr']). + ',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randcode(16))))); + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to add API key. Internal error'; + } + + return false; + } +} + +/** + * Controller for API methods. Provides methods to check to make sure the + * API key was sent and that the Client-IP and API-Key have been registered + * in the database, and methods for parsing and validating data sent in the + * API request. + */ +class ApiController { + function requireApiKey() { + # Validate the API key -- required to be sent via the X-API-Key + # header + if (!isset($_SERVER['HTTP_X_API_KEY'])) + Http::response(403, "API key required"); + else if (!Api::validate($_SERVER['HTTP_X_API_KEY'], + $_SERVER['REMOTE_ADDR'])) + Http::response(401, + "API key not found or source IP not authorized"); + } + /** + * Retrieves the body of the API request and converts it to a common + * hashtable. For JSON formats, this is mostly a noop, the conversion + * work will be done for XML requests + */ + function getRequest($format) { + if (!($stream = @fopen("php://input", "r"))) + Http::response(400, "Unable to read request body"); + if ($format == "xml") { + if (!function_exists("xml_parser_create")) + Http::response(500, "XML extension not supported"); + $tree = new ApiXmlDataParser(); + } elseif ($format == "json") { + $tree = new ApiJsonDataParser(); + } + if (!($data = $tree->parse($stream))) + Http::response(400, $tree->lastError()); + $this->validate($data, $this->getRequestStructure($format)); + return $data; + } + /** + * Structure to validate the request against -- must be overridden to be + * useful + */ + function getRequestStructure($format) { return array(); } + /** + * Simple validation that makes sure the keys of a parsed request are + * expected. It is assumed that the functions actually implementing the + * API will further validate the contents of the request + */ + function validate($data, $structure, $prefix="") { + foreach ($data as $key=>$info) { + if (is_array($structure) and is_array($info)) { + $search = isset($structure[$key]) ? $key : "*"; + if (isset($structure[$search])) { + $this->validate($info, $structure[$search], "$prefix$key/"); + continue; + } + } elseif (in_array($key, $structure)) { + continue; + } + Http::response(400, "$prefix$key: Unexpected data received"); + } + } +} + +include_once "class.xml.php"; +class ApiXmlDataParser extends XmlDataParser { + + function parse($stream) { + return $this->fixup(parent::parse($stream)); + } + /** + * Perform simple operations to make data consistent between JSON and + * XML data types + */ + function fixup($current) { + if (!is_array($current)) + return $current; + foreach ($current as $key=>&$value) { + if ($key == "phone") { + $current["phone_ext"] = $value["ext"]; # PHP [like] point + $value = $value[":text"]; + } else if ($key == "alert") { + $value = (bool)$value; + } else if ($key == "autorespond") { + $value = (bool)$value; + } else if ($key == "attachments") { + foreach ($value as &$info) { + $info["data"] = $info[":text"]; + unset($info[":text"]); + } + unset($info); + } + if (is_array($value)) { + $value = $this->fixup($value); + } + } + return $current; + } +} + +include_once "class.json.php"; +class ApiJsonDataParser extends JsonDataParser { + function parse($stream) { + return $this->fixup(parent::parse($stream)); + } + function fixup($current) { + if (!is_array($current)) + return $current; + foreach ($current as $key=>&$value) { + if ($key == "phone") { + list($value,$current["phone_ext"]) + = explode("X", strtoupper($value), 2); + } else if ($key == "alert") { + $value = (bool)$value; + } else if ($key == "autorespond") { + $value = (bool)$value; + } else if ($key == "attachments") { + foreach ($value as &$info) { + $data = reset($info); + # PHP5: fopen("data://$data[5:]"); + if (substr($data, 0, 5) != "data:") { + $info = array( + "data" => $data, + "type" => "text/plain", + "name" => key($info)); + } else { + $data = substr($data,5); + list($meta, $contents) = explode(",", $data); + list($type, $extra) = explode(";", $meta); + $info = array( + "data" => $contents, + "type" => $type, + "name" => key($info)); + if (substr($extra, -6) == "base64") + $info["encoding"] = "base64"; + # TODO: Handle 'charset' hint in $extra + } + } + unset($value); + } + if (is_array($value)) { + $value = $this->fixup($value); + } + } + return $current; + } +} + +?> diff --git a/include/class.attachment.php b/include/class.attachment.php new file mode 100644 index 0000000000000000000000000000000000000000..8644d671c4986f6b5541e6b9b592fa06fde91652 --- /dev/null +++ b/include/class.attachment.php @@ -0,0 +1,103 @@ +<?php +/********************************************************************* + class.attachment.php + + Attachment Handler - mainly used for lookup...doesn't save! + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.file.php'); + +class Attachment { + var $id; + var $file_id; + var $ticket_id; + + var $info; + + function Attachment($id,$tid=0) { + + $sql='SELECT * FROM '.TICKET_ATTACHMENT_TABLE.' WHERE attach_id='.db_input($id); + if($tid) + $sql.=' AND ticket_id='.db_input($tid); + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + + $this->id=$this->ht['attach_id']; + $this->file_id=$this->ht['file_id']; + $this->ticket_id=$this->ht['ticket_id']; + + $this->file=null; + $this->ticket=null; + + return true; + } + + function getId() { + return $this->id; + } + + function getTicketId() { + return $this->ticket_id; + } + + function getTicket() { + if(!$this->ticket && $this->getTicketId()) + $this->ticket = Ticket::lookup($this->getTicketId()); + + return $this->ticket; + } + + function getFileId() { + return $this->file_id; + } + + function getFile() { + if(!$this->file && $this->getFileId()) + $this->file = AttachmentFile::lookup($this->getFileId()); + + return $this->file; + } + + function getCreateDate() { + return $this->ht['created']; + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + /* Static functions */ + function getIdByFileHash($hash, $tid=0) { + $sql='SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE.' a ' + .' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) ' + .' WHERE f.hash='.db_input($hash); + if($tid) + $sql.=' AND a.ticket_id='.db_input($tid); + + return db_result(db_query($sql)); + } + + function lookup($id,$tid=0) { + $id=is_numeric($id)?$id:self::getIdByFileHash($hash,$tid); + + return ($id && is_numeric($id) && ($attach = new Attachment($id,$tid)) && $attach->getId()==$id)?$attach:null; + } + +} +?> diff --git a/include/class.banlist.php b/include/class.banlist.php new file mode 100644 index 0000000000000000000000000000000000000000..27729b44a42f45cb0d187f1e8f2a8b731991784e --- /dev/null +++ b/include/class.banlist.php @@ -0,0 +1,66 @@ +<?php +/********************************************************************* + class.banlist.php + + Banned email addresses handle. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once "class.filter.php"; + +class Banlist { + + function add($email,$submitter='') { + return self::getSystemBanList()->addRule('email','equal',$email); + } + + function remove($email) { + return self::getSystemBanList()->removeRule('email','equal',$email); + } + + function isbanned($email) { + return EmailFilter::isBanned($email); + } + + function includes($email) { + return self::getSystemBanList()->containsRule('email','equal',$email); + } + + function ensureSystemBanList() { + + if (!($id=Filter::getIdByName('SYSTEM BAN LIST'))) + $id=self::createSystemBanList(); + + return $id; + } + + function createSystemBanList() { + # XXX: Filter::create should return the ID!!! + $errors=array(); + return Filter::create(array( + 'execorder' => 99, + 'name' => 'SYSTEM BAN LIST', + 'isactive' => 1, + 'match_all_rules' => false, + 'reject_email' => true, + 'rules' => array(), + 'notes' => 'Internal list for email banning. Do not remove' + ), $errors); + } + + function getSystemBanList() { + return new Filter(self::ensureSystemBanList()); + } + + function getFilter() { + return self::getSystemBanList(); + } +} diff --git a/include/class.canned.php b/include/class.canned.php new file mode 100644 index 0000000000000000000000000000000000000000..00c46de3365e92fef38fe29e154c56fafacb622b --- /dev/null +++ b/include/class.canned.php @@ -0,0 +1,272 @@ +<?php +/********************************************************************* + class.canned.php + + Canned Responses AKA Premade replies + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.file.php'); + +class Canned { + var $id; + var $ht; + + var $attachments; + + function Canned($id){ + $this->id=0; + $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT canned.*, count(attach.file_id) as attachments ' + .' FROM '.CANNED_TABLE.' canned ' + .' LEFT JOIN '.CANNED_ATTACHMENT_TABLE.' attach ON (attach.canned_id=canned.canned_id) ' + .' WHERE canned.canned_id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + + $this->ht = db_fetch_array($res); + $this->id = $this->ht['canned_id']; + $this->attachments = array(); + + return true; + } + + function reload() { + return $this->load(); + } + + function getId(){ + return $this->id; + } + + function isEnabled() { + return ($this->ht['isenabled']); + } + + function isActive(){ + return $this->isEnabled(); + } + + + function getTitle() { + return $this->ht['title']; + } + + function getResponse() { + return $this->ht['response']; + } + + function getReply() { + return $this->getResponse(); + } + + function getNotes() { + return $this->ht['notes']; + } + + function getDeptId(){ + return $this->ht['dept_id']; + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function update($vars, &$errors) { + + if(!$this->save($this->getId(),$vars,$errors)) + return false; + + $this->reload(); + + return true; + } + + function getNumAttachments() { + return $this->ht['attachments']; + } + + function getAttachments() { + + if(!$this->attachments && $this->getNumAttachments()) { + + $sql='SELECT f.id, f.size, f.hash, f.name ' + .' FROM '.FILE_TABLE.' f ' + .' INNER JOIN '.CANNED_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' + .' WHERE a.canned_id='.db_input($this->getId()); + + $this->attachments = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) { + $rec['key'] =md5($rec['id'].session_id().$rec['hash']); + $this->attachments[] = $rec; + } + } + } + + return $this->attachments; + } + /* + @files is an array - hash table of multiple attachments. + */ + function uploadAttachments($files) { + + foreach($files as $file) { + if(($fileId=is_numeric($file)?$file:AttachmentFile::upload($file)) && is_numeric($fileId)) { + $sql ='INSERT INTO '.CANNED_ATTACHMENT_TABLE + .' SET canned_id='.db_input($this->getId()).', file_id='.db_input($fileId); + if(db_query($sql)) $i++; + } + } + + if($i) $this->reload(); + + return $i; + } + + function deleteAttachment($fileId) { + + $sql='DELETE FROM '.CANNED_ATTACHMENT_TABLE + .' WHERE canned_id='.db_input($this->getId()) + .' AND file_id='.db_input($fileId) + .' LIMIT 1'; + + if(!db_query($sql) || !db_affected_rows()) + return false; + + + if(($file=AttachmentFile::lookup($fileId)) && !$file->isInuse()) + $file->delete(); + + return true; + } + + function deleteAttachments(){ + + $deleted=0; + if(($attachments = $this->getAttachments())) { + foreach($attachments as $attachment) + if($attachment['id'] && $this->deleteAttachment($attachment['id'])) + $deleted++; + } + + return $deleted; + } + + function delete(){ + + $sql='DELETE FROM '.CANNED_TABLE.' WHERE canned_id='.db_input($this->getId()).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())) { + $this->deleteAttachments(); + } + + return $num; + } + + /*** Static functions ***/ + function lookup($id){ + return ($id && is_numeric($id) && ($c= new Canned($id)) && $c->getId()==$id)?$c:null; + } + + function create($vars,&$errors) { + return self::save(0,$vars,$errors); + } + + function getIdByTitle($titke) { + $sql='SELECT canned_id FROM '.CANNED_TABLE.' WHERE title='.db_input($title); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function getCannedResponses($deptId=0, $explicit=false) { + + $sql='SELECT canned_id, title FROM '.CANNED_TABLE; + if($deptId){ + $sql.=' WHERE dept_id='.db_input($deptId); + if(!$explicit) + $sql.=' OR dept_id=0'; + } + $sql.=' ORDER BY title'; + + $responses = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id,$title)=db_fetch_row($res)) + $responses[$id]=$title; + } + + return $responses; + } + + function responsesByDeptId($deptId, $explicit=false) { + return self::getCannedResponses($deptId, $explicit); + } + + function save($id,$vars,&$errors) { + + //We're stripping html tags - until support is added to tickets. + $vars['title']=Format::striptags(trim($vars['title'])); + $vars['response']=Format::striptags(trim($vars['response'])); + $vars['notes']=Format::striptags(trim($vars['notes'])); + + if($id && $id!=$vars['id']) + $errors['err']='Internal error. Try again'; + + if(!$vars['title']) + $errors['title']='Title required'; + elseif(strlen($vars['title'])<3) + $errors['title']='Title is too short. 3 chars minimum'; + elseif(($cid=self::getIdByTitle($vars['title'])) && $cid!=$id) + $errors['title']='Title already exists'; + + if(!$vars['response']) + $errors['response']='Response text required'; + + if($errors) return false; + + $sql=' updated=NOW() '. + ',dept_id='.db_input($vars['dept_id']?$vars['dept_id']:0). + ',isenabled='.db_input($vars['isenabled']). + ',title='.db_input($vars['title']). + ',response='.db_input($vars['response']). + ',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.CANNED_TABLE.' SET '.$sql.' WHERE canned_id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update canned reply.'; + + } else { + $sql='INSERT INTO '.CANNED_TABLE.' SET '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create the canned reply. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.captcha.php b/include/class.captcha.php new file mode 100644 index 0000000000000000000000000000000000000000..1423735e61d42b0b6ccbf50e9377cbac81da96f3 --- /dev/null +++ b/include/class.captcha.php @@ -0,0 +1,54 @@ +<?php +/********************************************************************* + class.captcha.php + + Very basic captcha class. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Captcha { + var $hash; + var $bgimages=array('cottoncandy.png','grass.png','ripple.png','silk.png','whirlpool.png', + 'bubbles.png','crackle.png','lines.png','sand.png','snakeskin.png'); + var $font = 10; + function Captcha($len=6,$font=7,$bg=''){ + + $this->hash = strtoupper(substr(md5(rand(0, 9999)),rand(0, 24),$len)); + $this->font = $font; + + if($bg && !is_dir($bg)){ //bg file provided? + $this->bgimg=$bg; + }else{ //assume dir provided or defaults to local. + $this->bgimg=rtrim($bg,'/').'/'.$this->bgimages[array_rand($this->bgimages, 1)]; + } + } + + function getImage(){ + + if(!extension_loaded('gd') || !function_exists('gd_info')) //GD ext required. + return; + + $_SESSION['captcha'] =''; //Clear + + list($w,$h) = getimagesize($this->bgimg); + $x = round(($w/2)-((strlen($this->hash)*imagefontwidth($this->font))/2), 1); + $y = round(($h/2)-(imagefontheight($this->font)/2)); + + $img= imagecreatefrompng($this->bgimg); + imagestring($img,$this->font, $x, $y,$this->hash,imagecolorallocate($img,0, 0, 0)); + + Header ("(captcha-content-type:) image/png"); + imagepng($img); + imagedestroy($img); + $_SESSION['captcha'] = md5($this->hash); + } +} + +?> diff --git a/include/class.category.php b/include/class.category.php new file mode 100644 index 0000000000000000000000000000000000000000..2949b9a909bc2673866b2f48e9448e517686c30b --- /dev/null +++ b/include/class.category.php @@ -0,0 +1,167 @@ +<?php +/********************************************************************* + class.category.php + + Backend support for article categories. + + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Category { + var $id; + var $ht; + + function Category($id) { + $this->id=0; + $this->load($id); + } + + function load($id) { + + $sql=' SELECT cat.*,count(faq.faq_id) as faqs ' + .' FROM '.FAQ_CATEGORY_TABLE.' cat ' + .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) ' + .' WHERE cat.category_id='.db_input($id) + .' GROUP BY cat.category_id'; + + if (!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht = db_fetch_array($res); + $this->id = $this->ht['category_id']; + + return true; + } + + function reload() { + return $this->load($this->getId()); + } + + /* ------------------> Getter methods <--------------------- */ + function getId() { return $this->id; } + function getName() { return $this->ht['name']; } + function getNumFAQs() { return $this->ht['faqs']; } + function getDescription() { return $this->ht['description']; } + function getNotes() { return $this->ht['notes']; } + function getCreateDate() { return $this->ht['created']; } + function getUpdateDate() { return $this->ht['updated']; } + + function isPublic() { return ($this->ht['ispublic']); } + function getHashtable() { return $this->ht; } + + /* ------------------> Setter methods <--------------------- */ + function setName($name) { $this->ht['name']=$name; } + function setNotes($notes) { $this->ht['notes']=$notes; } + function setDescription($desc) { $this->ht['description']=$desc; } + + /* --------------> Database access methods <---------------- */ + function update($vars, &$errors) { + + if(!$this->save($this->getId(), $vars, $errors)) + return false; + + //TODO: move FAQs if requested. + + $this->reload(); + + return true; + } + + function delete() { + + $sql='DELETE FROM '.FAQ_CATEGORY_TABLE + .' WHERE category_id='.db_input($this->getId()) + .' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())) { + db_query('DELETE FROM '.FAQ_TABLE + .' WHERE category_id='.db_input($this->getId())); + + } + + return $num; + } + + /* ------------------> Static methods <--------------------- */ + + function lookup($id) { + return ($id && is_numeric($id) && ($c = new Category($id)))?$c:null; + } + + function findIdByName($name) { + $sql='SELECT category_id FROM '.FAQ_CATEGORY_TABLE.' WHERE name='.db_input($name); + list($id) = db_fetch_row(db_query($sql)); + + return $id; + } + + function findByName($name) { + if(($id=self::findIdByName($name))) + return new Category($id); + + return false; + } + + function validate($vars, &$errors) { + return self::save(0, $vars, $errors,true); + } + + function create($vars, &$errors) { + return self::save(0, $vars, $errors); + } + + function save($id, $vars, &$errors, $validation=false) { + + //Cleanup. + $vars['name']=Format::striptags(trim($vars['name'])); + + //validate + if($id && $id!=$vars['id']) + $errors['err']='Internal error. Try again'; + + if(!$vars['name']) + $errors['name']='Category name is required'; + elseif(strlen($vars['name'])<3) + $errors['name']='Name is too short. 3 chars minimum'; + elseif(($cid=self::findIdByName($vars['name'])) && $cid!=$id) + $errors['name']='Category already exists'; + + if(!$vars['description']) + $errors['description']='Category description is required'; + + if($errors) return false; + + /* validation only */ + if($validation) return true; + + //save + $sql=' updated=NOW() '. + ',ispublic='.db_input(isset($vars['ispublic'])?$vars['ispublic']:0). + ',name='.db_input($vars['name']). + ',description='.db_input(Format::safe_html($vars['description'])). + ',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET '.$sql.' WHERE category_id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update FAQ category.'; + + } else { + $sql='INSERT INTO '.FAQ_CATEGORY_TABLE.' SET '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create FAQ category. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.client.php b/include/class.client.php new file mode 100644 index 0000000000000000000000000000000000000000..f7ede15c52fe33b0a1a99dfdecb8081c64407d0d --- /dev/null +++ b/include/class.client.php @@ -0,0 +1,93 @@ +<?php +/********************************************************************* + class.client.php + + Handles everything about client + + The class will undergo major changes one client's accounts are used. + At the moment we will play off the email + ticket ID authentication. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Client { + + + var $id; + var $fullname; + var $username; + var $passwd; + var $email; + + + var $udata; + var $ticket_id; + var $ticketID; + + function Client($email,$id){ + $this->id =0; + $this->load($id,$email); + } + + function isClient(){ + return TRUE; + } + + function load($id,$email=''){ + + $sql='SELECT ticket_id,ticketID,name,email FROM '.TICKET_TABLE.' WHERE ticketID='.db_input($id); + if($email){ //don't validate...using whatever is entered. + $sql.=' AND email='.db_input($email); + } + $res=db_query($sql); + if(!$res || !db_num_rows($res)) + return NULL; + + $row=db_fetch_array($res); + $this->udata=$row; + $this->id = $row['ticketID']; //placeholder + $this->ticket_id = $row['ticket_id']; + $this->ticketID = $row['ticketID']; + $this->fullname = ucfirst($row['name']); + $this->username = $row['email']; + $this->email = $row['email']; + + return($this->id); + } + + + function getId(){ + return $this->id; + } + + function getEmail(){ + return $this->email; + } + + function getUserName(){ + return $this->username; + } + + function getName(){ + return $this->fullname; + } + + function getTicketID() { + return $this->ticketID; + } + + /* ------------- Static ---------------*/ + function lookup($id, $email) { + return ($id && is_numeric($id) && ($c=new Client($id,$email)) && $c->getId()==$id)?$c:null; + } + +} + +?> diff --git a/include/class.config.php b/include/class.config.php new file mode 100644 index 0000000000000000000000000000000000000000..1e3d7745ffaf9788715308564bcc676fc28ac845 --- /dev/null +++ b/include/class.config.php @@ -0,0 +1,841 @@ +<?php +/********************************************************************* + class.config.php + + osTicket config info manager. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once(INCLUDE_DIR.'class.email.php'); + +class Config { + + var $id=0; + var $mysqltzoffset=0; + var $config=array(); + + var $defaultDept; //Default Department + var $defaultSLA; //Default SLA + var $defaultEmail; //Default Email + var $alertEmail; //Alert Email + var $defaultSMTPEmail; //Default SMTP Email + + function Config($id) { + $this->load($id); + } + + function load($id) { + + $sql='SELECT * FROM '.CONFIG_TABLE.' WHERE id='.db_input($id); + if($id && ($res=db_query($sql)) && db_num_rows($res)) { + $this->config=db_fetch_array($res); + $this->id=$this->config['id']; + + return true; + } + + return false; + } + + //Initialize some default values. + function init() { + list($mysqltz)=db_fetch_row(db_query('SELECT @@session.time_zone ')); + $this->setMysqlTZ($mysqltz); + } + + function reload() { + if($this->load($this->id)) + $this->init(); + } + + + function isHelpDeskOffline() { + return !$this->isSystemOnline(); + } + + function isSystemOnline() { + return ($this->config['isonline']); + } + + function isKnowledgebaseEnabled() { + + require_once(INCLUDE_DIR.'class.faq.php'); + return ($this->config['enable_kb'] && FAQ::countPublishedFAQs()); + } + + function getVersion() { + return '1.7 DPR'; + } + + function getSchemaVersion() { + return $this->config['schema_signature']; + } + + function setMysqlTZ($tz) { + //TODO: Combine the 2 replace regex + if($tz=='SYSTEM') + $this->mysqltzoffset=preg_replace('/([+-]\d{2})(\d{2})/','\1',date('O')); + else + $this->mysqltzoffset=preg_replace('/([+-]\d{2})(:)(\d{2})/','\1',$tz); + } + + function getMysqlTZoffset() { + return $this->mysqltzoffset; + } + + /* Date & Time Formats */ + function observeDaylightSaving() { + return ($this->config['enable_daylight_saving']); + } + function getTimeFormat() { + return $this->config['time_format']; + } + function getDateFormat() { + return $this->config['date_format']; + } + + function getDateTimeFormat() { + return $this->config['datetime_format']; + } + + function getDayDateTimeFormat() { + return $this->config['daydatetime_format']; + } + + function getId() { + return $this->config['id']; + } + + function getTitle() { + return $this->config['helpdesk_title']; + } + + function getUrl() { + return $this->config['helpdesk_url']; + } + + function getBaseUrl() { //Same as above with no trailing slash. + return rtrim($this->getUrl(),'/'); + } + + function getConfig() { + return $this->config; + } + + function getTZOffset() { + return $this->config['timezone_offset']; + } + + function getPageSize() { + return $this->config['max_page_size']; + } + + function getGracePeriod() { + return $this->config['overdue_grace_period']; + } + + function getPasswdResetPeriod() { + return $this->config['passwd_reset_period']; + } + + function getClientTimeout() { + return $this->getClientSessionTimeout(); + } + + function getClientSessionTimeout() { + return $this->config['client_session_timeout']*60; + } + + function getClientLoginTimeout() { + return $this->config['client_login_timeout']*60; + } + + function getClientMaxLogins() { + return $this->config['client_max_logins']; + } + + function getStaffTimeout() { + return $this->getStaffSessionTimeout(); + } + + function getStaffSessionTimeout() { + return $this->config['staff_session_timeout']*60; + } + + function getStaffLoginTimeout() { + return $this->config['staff_login_timeout']*60; + } + + function getStaffMaxLogins() { + return $this->config['staff_max_logins']; + } + + function getLockTime() { + return $this->config['autolock_minutes']; + } + + function getDefaultDeptId() { + return $this->config['default_dept_id']; + } + + function getDefaultDept() { + + if(!$this->defaultDept && $this->getDefaultDeptId()) + $this->defaultDept=Dept::lookup($this->getDefaultDeptId()); + + return $this->defaultDept; + } + + function getDefaultEmailId() { + return $this->config['default_email_id']; + } + + function getDefaultEmail() { + + if(!$this->defaultEmail && $this->getDefaultEmailId()) + $this->defaultEmail=Email::lookup($this->getDefaultEmailId()); + + return $this->defaultEmail; + } + + function getDefaultEmailAddress() { + $email=$this->getDefaultEmail(); + return $email?$email->getAddress():null; + } + + function getDefaultSLAId() { + return $this->config['default_sla_id']; + } + + function getDefaultSLA() { + + if(!$this->defaultSLA && $this->getDefaultSLAId()) + $this->defaultSLA=SLA::lookup($this->getDefaultSLAId()); + + return $this->defaultSLA; + } + + function getAlertEmailId() { + return $this->config['alert_email_id']; + } + + function getAlertEmail() { + + if(!$this->alertEmail && $this->config['alert_email_id']) + $this->alertEmail= new Email($this->config['alert_email_id']); + return $this->alertEmail; + } + + function getDefaultSMTPEmail() { + + if(!$this->defaultSMTPEmail && $this->config['default_smtp_id']) + $this->defaultSMTPEmail= new Email($this->config['default_smtp_id']); + return $this->defaultSMTPEmail; + } + + function allowSMTPSpoofing() { + return $this->config['spoof_default_smtp']; + } + + function getDefaultPriorityId() { + return $this->config['default_priority_id']; + } + + function getDefaultTemplateId() { + return $this->config['default_template_id']; + } + + function getDefaultTemplate() { + + if(!$this->defaultTemplate && $this->getDefaultTemplateId()) + $this->defaultTemplate = Template::lookup($this->getDefaultTemplateId()); + + return $this->defaultTemplate; + } + + function getMaxOpenTickets() { + return $this->config['max_open_tickets']; + } + + function getMaxFileSize() { + return $this->config['max_file_size']; + } + + function getMaxFileUploads() { + return $this->config['max_staff_file_uploads']; + } + + function getLogLevel() { + return $this->config['log_level']; + } + + function getLogGracePeriod() { + return $this->config['log_graceperiod']; + } + + function logTicketActivity() { + return $this->config['log_ticket_activity']; + } + + function clickableURLS() { + return ($this->config['clickable_urls']); + } + + function canFetchMail() { + return ($this->config['enable_mail_fetch']); + } + + function enableStaffIPBinding() { + return ($this->config['staff_ip_binding']); + } + + function enableCaptcha() { + return (extension_loaded('gd') && function_exists('gd_info') && $this->config['enable_captcha']); + } + + function enableAutoCron() { + return ($this->config['enable_auto_cron']); + } + + function enableEmailPiping() { + return ($this->config['enable_email_piping']); + } + + function allowPriorityChange() { + return ($this->config['allow_priority_change']); + } + + + function useEmailPriority() { + return ($this->config['use_email_priority']); + } + + function getAdminEmail() { + return $this->config['admin_email']; + } + + function getReplySeparator() { + return $this->config['reply_separator']; + } + + function stripQuotedReply() { + return ($this->config['strip_quoted_reply']); + } + + function saveEmailHeaders() { + return true; //No longer an option...hint: big plans for headers coming!! + } + + function useRandomIds() { + return ($this->config['random_ticket_ids']); + } + + /* autoresponders & Alerts */ + function autoRespONNewTicket() { + return ($this->config['ticket_autoresponder']); + } + + function autoRespONNewMessage() { + return ($this->config['message_autoresponder']); + } + + function notifyONNewStaffTicket() { + return ($this->config['ticket_notice_active']); + } + + function alertONNewMessage() { + return ($this->config['message_alert_active']); + } + + function alertLastRespondentONNewMessage() { + return ($this->config['message_alert_laststaff']); + } + + function alertAssignedONNewMessage() { + return ($this->config['message_alert_assigned']); + } + + function alertDeptManagerONNewMessage() { + return ($this->config['message_alert_dept_manager']); + } + + function alertONNewNote() { + return ($this->config['note_alert_active']); + } + + function alertLastRespondentONNewNote() { + return ($this->config['note_alert_laststaff']); + } + + function alertAssignedONNewNote() { + return ($this->config['note_alert_assigned']); + } + + function alertDeptManagerONNewNote() { + return ($this->config['note_alert_dept_manager']); + } + + function alertONNewTicket() { + return ($this->config['ticket_alert_active']); + } + + function alertAdminONNewTicket() { + return ($this->config['ticket_alert_admin']); + } + + function alertDeptManagerONNewTicket() { + return ($this->config['ticket_alert_dept_manager']); + } + + function alertDeptMembersONNewTicket() { + return ($this->config['ticket_alert_dept_members']); + } + + function alertONTransfer() { + return ($this->config['transfer_alert_active']); + } + + function alertAssignedONTransfer() { + return ($this->config['transfer_alert_assigned']); + } + + function alertDeptManagerONTransfer() { + return ($this->config['transfer_alert_dept_manager']); + } + + function alertDeptMembersONTransfer() { + return ($this->config['transfer_alert_dept_members']); + } + + function alertONAssignment() { + return ($this->config['assigned_alert_active']); + } + + function alertStaffONAssignment() { + return ($this->config['assigned_alert_staff']); + } + + function alertTeamLeadONAssignment() { + return ($this->config['assigned_alert_team_lead']); + } + + function alertTeamMembersONAssignment() { + return ($this->config['assigned_alert_team_members']); + } + + + function alertONOverdueTicket() { + return ($this->config['overdue_alert_active']); + } + + function alertAssignedONOverdueTicket() { + return ($this->config['overdue_alert_assigned']); + } + + function alertDeptManagerONOverdueTicket() { + return ($this->config['overdue_alert_dept_manager']); + } + + function alertDeptMembersONOverdueTicket() { + return ($this->config['overdue_alert_dept_members']); + } + + function autoAssignReopenedTickets() { + return ($this->config['auto_assign_reopened_tickets']); + } + + function showAssignedTickets() { + return ($this->config['show_assigned_tickets']); + } + + function showAnsweredTickets() { + return ($this->config['show_answered_tickets']); + } + + function hideStaffName() { + return ($this->config['hide_staff_name']); + } + + function sendOverLimitNotice() { + return ($this->config['overlimit_notice_active']); + } + + /* Error alerts sent to admin email when enabled */ + function alertONSQLError() { + return ($this->config['send_sql_errors']); + } + function alertONLoginError() { + return ($this->config['send_login_errors']); + } + + function alertONMailParseError() { + return ($this->config['send_mailparse_errors']); + } + + + + /* Attachments */ + + function emailAttachments() { + return ($this->config['email_attachments']); + } + + function allowAttachments() { + return ($this->config['allow_attachments']); + } + + function allowOnlineAttachments() { + return ($this->allowAttachments() && $this->config['allow_online_attachments']); + } + + function allowAttachmentsOnlogin() { + return ($this->allowOnlineAttachments() && $this->config['allow_online_attachments_onlogin']); + } + + function allowEmailAttachments() { + return ($this->allowAttachments() && $this->config['allow_email_attachments']); + } + + function getUploadDir() { + return $this->config['upload_dir']; + } + + //simply checking if destination dir is usable..nothing to do with permission to upload! + function canUploadFiles() { + $dir=$this->config['upload_dir']; + return ($dir && is_writable($dir))?TRUE:FALSE; + } + + function canUploadFileType($filename) { + $ext = strtolower(preg_replace("/.*\.(.{3,4})$/", "$1", $filename)); + $allowed=$this->config['allowed_filetypes']?array_map('trim',explode(',',strtolower($this->config['allowed_filetypes']))):null; + return ($ext && is_array($allowed) && (in_array(".$ext",$allowed) || in_array(".*",$allowed)))?TRUE:FALSE; + } + + function updateSettings($vars,&$errors) { + + if(!$vars || $errors) + return false; + + switch(strtolower($vars['t'])) { + case 'general': + return $this->updateGeneralSetting($vars,$errors); + break; + case 'dates': + return $this->updateDateTimeSetting($vars,$errors); + break; + case 'tickets': + return $this->updateTicketsSetting($vars,$errors); + break; + case 'emails': + return $this->updateEmailsSetting($vars,$errors); + break; + case 'attachments': + return $this->updateAttachmentsSetting($vars,$errors); + break; + case 'autoresponders': + return $this->updateAutoresponderSetting($vars,$errors); + break; + case 'alerts': + return $this->updateAlertsSetting($vars,$errors); + break; + case 'kb': + return $this->updateKBSetting($vars,$errors); + break; + default: + $errors['err']='Unknown setting option. Get technical support.'; + } + + return false; + } + + function updateGeneralSetting($vars,&$errors) { + + $f=array(); + $f['helpdesk_url']=array('type'=>'string', 'required'=>1, 'error'=>'Helpdesk URl required'); + $f['helpdesk_title']=array('type'=>'string', 'required'=>1, 'error'=>'Helpdesk title required'); + $f['default_dept_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default Dept. required'); + $f['default_template_id']=array('type'=>'int', 'required'=>1, 'error'=>'You must select template.'); + $f['staff_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); + $f['client_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); + + if(!Validator::process($f,$vars,$errors) || $errors) + return false; + + $sql='UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',isonline='.db_input($vars['isonline']). + ',helpdesk_title='.db_input($vars['helpdesk_title']). + ',helpdesk_url='.db_input($vars['helpdesk_url']). + ',default_dept_id='.db_input($vars['default_dept_id']). + ',default_template_id='.db_input($vars['default_template_id']). + ',max_page_size='.db_input($vars['max_page_size']). + ',log_level='.db_input($vars['log_level']). + ',log_graceperiod='.db_input($vars['log_graceperiod']). + ',passwd_reset_period='.db_input($vars['passwd_reset_period']). + ',staff_max_logins='.db_input($vars['staff_max_logins']). + ',staff_login_timeout='.db_input($vars['staff_login_timeout']). + ',staff_session_timeout='.db_input($vars['staff_session_timeout']). + ',staff_ip_binding='.db_input(isset($vars['staff_ip_binding'])?1:0). + ',client_max_logins='.db_input($vars['client_max_logins']). + ',client_login_timeout='.db_input($vars['client_login_timeout']). + ',client_session_timeout='.db_input($vars['client_session_timeout']). + ',clickable_urls='.db_input(isset($vars['clickable_urls'])?1:0). + ',enable_auto_cron='.db_input(isset($vars['enable_auto_cron'])?1:0). + ' WHERE id='.$this->getId(); + + return (db_query($sql)); + } + + function updateDateTimeSetting($vars,&$errors) { + + $f=array(); + $f['time_format']=array('type'=>'string', 'required'=>1, 'error'=>'Time format required'); + $f['date_format']=array('type'=>'string', 'required'=>1, 'error'=>'Date format required'); + $f['datetime_format']=array('type'=>'string', 'required'=>1, 'error'=>'Datetime format required'); + $f['daydatetime_format']=array('type'=>'string', 'required'=>1, 'error'=>'Day, Datetime format required'); + $f['default_timezone_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default Timezone required'); + + if(!Validator::process($f,$vars,$errors) || $errors) + return false; + + $sql='UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',time_format='.db_input($vars['time_format']). + ',date_format='.db_input($vars['date_format']). + ',datetime_format='.db_input($vars['datetime_format']). + ',daydatetime_format='.db_input($vars['daydatetime_format']). + ',default_timezone_id='.db_input($vars['default_timezone_id']). + ',enable_daylight_saving='.db_input(isset($vars['enable_daylight_saving'])?1:0). + ' WHERE id='.$this->getId(); + + return (db_query($sql)); + } + + function updateTicketsSetting($vars,&$errors) { + + + $f=array(); + $f['default_sla_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); + $f['default_priority_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); + $f['max_open_tickets']=array('type'=>'int', 'required'=>1, 'error'=>'Enter valid numeric value'); + $f['autolock_minutes']=array('type'=>'int', 'required'=>1, 'error'=>'Enter lock time in minutes'); + + + if($vars['enable_captcha']) { + if (!extension_loaded('gd')) + $errors['enable_captcha']='The GD extension required'; + elseif(!function_exists('imagepng')) + $errors['enable_captcha']='PNG support required for Image Captcha'; + } + + if(!Validator::process($f,$vars,$errors) || $errors) + return false; + + $sql='UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',random_ticket_ids='.db_input($vars['random_ticket_ids']). + ',default_priority_id='.db_input($vars['default_priority_id']). + ',default_sla_id='.db_input($vars['default_sla_id']). + ',max_open_tickets='.db_input($vars['max_open_tickets']). + ',autolock_minutes='.db_input($vars['autolock_minutes']). + ',allow_priority_change='.db_input(isset($vars['allow_priority_change'])?1:0). + ',use_email_priority='.db_input(isset($vars['use_email_priority'])?1:0). + ',enable_captcha='.db_input(isset($vars['enable_captcha'])?1:0). + ',log_ticket_activity='.db_input(isset($vars['log_ticket_activity'])?1:0). + ',auto_assign_reopened_tickets='.db_input(isset($vars['auto_assign_reopened_tickets'])?1:0). + ',show_assigned_tickets='.db_input(isset($vars['show_assigned_tickets'])?1:0). + ',show_answered_tickets='.db_input(isset($vars['show_answered_tickets'])?1:0). + ',show_related_tickets='.db_input(isset($vars['show_related_tickets'])?1:0). + ',hide_staff_name='.db_input(isset($vars['hide_staff_name'])?1:0); + + return (db_query($sql)); + } + + + function updateEmailsSetting($vars,&$errors) { + + $f=array(); + $f['default_email_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default email required'); + $f['alert_email_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); + $f['admin_email']=array('type'=>'email', 'required'=>1, 'error'=>'System admin email required'); + + if($vars['strip_quoted_reply'] && !$vars['reply_separator']) + $errors['reply_separator']='Reply separator required to strip quoted reply.'; + + if($vars['admin_email'] && Email::getIdByEmail($vars['admin_email'])) //Make sure admin email is not also a system email. + $errors['admin_email']='Email already setup as system email'; + + if(!Validator::process($f,$vars,$errors) || $errors) + return false; + + $sql='UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',default_email_id='.db_input($vars['default_email_id']). + ',alert_email_id='.db_input($vars['alert_email_id']). + ',default_smtp_id='.db_input($vars['default_smtp_id']). + ',admin_email='.db_input($vars['admin_email']). + ',enable_mail_polling='.db_input(isset($vars['enable_mail_polling'])?1:0). + ',enable_email_piping='.db_input(isset($vars['enable_email_piping'])?1:0). + ',strip_quoted_reply='.db_input(isset($vars['strip_quoted_reply'])?1:0). + ',reply_separator='.db_input($vars['reply_separator']); + + return (db_query($sql)); + } + + function updateAttachmentsSetting($vars,&$errors) { + + + if($vars['allow_attachments']) { + + if(!ini_get('file_uploads')) + $errors['err']='The \'file_uploads\' directive is disabled in php.ini'; + + if(!is_numeric($vars['max_file_size'])) + $errors['max_file_size']='Maximum file size required'; + + if(!$vars['allowed_filetypes']) + $errors['allowed_filetypes']='Allowed file extentions required'; + + if(!($maxfileuploads=ini_get('max_file_uploads'))) + $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; + + if(!$vars['max_user_file_uploads'] || $vars['max_user_file_uploads']>$maxfileuploads) + $errors['max_user_file_uploads']='Invalid selection'; + + if(!$vars['max_staff_file_uploads'] || $vars['max_staff_file_uploads']>$maxfileuploads) + $errors['max_staff_file_uploads']='Invalid selection'; + } + + if($errors) return false; + + $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',allow_attachments='.db_input(isset($vars['allow_attachments'])?1:0). + ',allowed_filetypes='.db_input(strtolower(preg_replace("/\n\r|\r\n|\n|\r/", '',trim($vars['allowed_filetypes'])))). + ',max_file_size='.db_input($vars['max_file_size']). + ',max_user_file_uploads='.db_input($vars['max_user_file_uploads']). + ',max_staff_file_uploads='.db_input($vars['max_staff_file_uploads']). + ',email_attachments='.db_input(isset($vars['email_attachments'])?1:0). + ',allow_email_attachments='.db_input(isset($vars['allow_email_attachments'])?1:0). + ',allow_online_attachments='.db_input(isset($vars['allow_online_attachments'])?1:0). + ',allow_online_attachments_onlogin='.db_input(isset($vars['allow_online_attachments_onlogin'])?1:0). + ' WHERE id='.db_input($this->getId()); + + + return (db_query($sql)); + + } + + + function updateAutoresponderSetting($vars,&$errors) { + + if($errors) return false; + + $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',ticket_autoresponder='.db_input($vars['ticket_autoresponder']). + ',message_autoresponder='.db_input($vars['message_autoresponder']). + ',ticket_notice_active='.db_input($vars['ticket_notice_active']). + ',overlimit_notice_active='.db_input($vars['overlimit_notice_active']); + + return (db_query($sql)); + + } + + + function updateKBSetting($vars,&$errors) { + + if($errors) return false; + + $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',enable_kb='.db_input(isset($vars['enable_kb'])?1:0). + ',enable_premade='.db_input(isset($vars['enable_premade'])?1:0); + + return (db_query($sql)); + } + + + function updateAlertsSetting($vars,&$errors) { + + + if($vars['ticket_alert_active'] + && (!isset($vars['ticket_alert_admin']) + && !isset($vars['ticket_alert_dept_manager']) + && !isset($vars['ticket_alert_dept_members']))) { + $errors['ticket_alert_active']='Select recipient(s)'; + } + if($vars['message_alert_active'] + && (!isset($vars['message_alert_laststaff']) + && !isset($vars['message_alert_assigned']) + && !isset($vars['message_alert_dept_manager']))) { + $errors['message_alert_active']='Select recipient(s)'; + } + + if($vars['note_alert_active'] + && (!isset($vars['note_alert_laststaff']) + && !isset($vars['note_alert_assigned']) + && !isset($vars['note_alert_dept_manager']))) { + $errors['note_alert_active']='Select recipient(s)'; + } + + if($vars['transfer_alert_active'] + && (!isset($vars['transfer_alert_assigned']) + && !isset($vars['transfer_alert_dept_manager']) + && !isset($vars['transfer_alert_dept_members']))) { + $errors['transfer_alert_active']='Select recipient(s)'; + } + + if($vars['overdue_alert_active'] + && (!isset($vars['overdue_alert_assigned']) + && !isset($vars['overdue_alert_dept_manager']) + && !isset($vars['overdue_alert_dept_members']))) { + $errors['overdue_alert_active']='Select recipient(s)'; + } + + if($vars['assigned_alert_active'] + && (!isset($vars['assigned_alert_staff']) + && !isset($vars['assigned_alert_team_lead']) + && !isset($vars['assigned_alert_team_members']))) { + $errors['assigned_alert_active']='Select recipient(s)'; + } + + if($errors) return false; + + $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() '. + ',ticket_notice_active='.db_input($vars['ticket_notice_active']). + ',ticket_alert_active='.db_input($vars['ticket_alert_active']). + ',ticket_alert_admin='.db_input(isset($vars['ticket_alert_admin'])?1:0). + ',ticket_alert_dept_manager='.db_input(isset($vars['ticket_alert_dept_manager'])?1:0). + ',ticket_alert_dept_members='.db_input(isset($vars['ticket_alert_dept_members'])?1:0). + ',message_alert_active='.db_input($vars['message_alert_active']). + ',message_alert_laststaff='.db_input(isset($vars['message_alert_laststaff'])?1:0). + ',message_alert_assigned='.db_input(isset($vars['message_alert_assigned'])?1:0). + ',message_alert_dept_manager='.db_input(isset($vars['message_alert_dept_manager'])?1:0). + ',note_alert_active='.db_input($vars['note_alert_active']). + ',note_alert_laststaff='.db_input(isset($vars['note_alert_laststaff'])?1:0). + ',note_alert_assigned='.db_input(isset($vars['note_alert_assigned'])?1:0). + ',note_alert_dept_manager='.db_input(isset($vars['note_alert_dept_manager'])?1:0). + ',assigned_alert_active='.db_input($vars['assigned_alert_active']). + ',assigned_alert_staff='.db_input(isset($vars['assigned_alert_staff'])?1:0). + ',assigned_alert_team_lead='.db_input(isset($vars['assigned_alert_team_lead'])?1:0). + ',assigned_alert_team_members='.db_input(isset($vars['assigned_alert_team_members'])?1:0). + ',transfer_alert_active='.db_input($vars['transfer_alert_active']). + ',transfer_alert_assigned='.db_input(isset($vars['transfer_alert_assigned'])?1:0). + ',transfer_alert_dept_manager='.db_input(isset($vars['transfer_alert_dept_manager'])?1:0). + ',transfer_alert_dept_members='.db_input(isset($var['transfer_alert_dept_members'])?1:0). + ',overdue_alert_active='.db_input($vars['overdue_alert_active']). + ',overdue_alert_assigned='.db_input(isset($vars['overdue_alert_assigned'])?1:0). + ',overdue_alert_dept_manager='.db_input(isset($vars['overdue_alert_dept_manager'])?1:0). + ',overdue_alert_dept_members='.db_input(isset($var['overdue_alert_dept_members'])?1:0). + ',send_sys_errors='.db_input(isset($vars['send_sys_errors'])?1:0). + ',send_sql_errors='.db_input(isset($vars['send_sql_errors'])?1:0). + ',send_login_errors='.db_input(isset($vars['send_login_errors'])?1:0); + + return (db_query($sql)); + + } +} +?> diff --git a/include/class.cron.php b/include/class.cron.php new file mode 100644 index 0000000000000000000000000000000000000000..3fd2d652cad8551ffa9852cd3d1f8984f060d542 --- /dev/null +++ b/include/class.cron.php @@ -0,0 +1,43 @@ +<?php +/********************************************************************* + class.cron.php + + Nothing special...just a central location for all cron calls. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + TODO: The plan is to make cron jobs db based. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +//TODO: Make it DB based! +class Cron { + + function MailFetcher() { + require_once(INCLUDE_DIR.'class.mailfetch.php'); + MailFetcher::fetchMail(); //Fetch mail..frequency is limited by email account setting. + } + + function TicketMonitor() { + require_once(INCLUDE_DIR.'class.ticket.php'); + require_once(INCLUDE_DIR.'class.lock.php'); + Ticket::checkOverdue(); //Make stale tickets overdue + TicketLock::cleanup(); //Remove expired locks + } + + function PurgeLogs() { + Sys::purgeLogs(); + } + + function run(){ //called by outside cron NOT autocron + Cron::MailFetcher(); + Cron::TicketMonitor(); + cron::PurgeLogs(); + } +} +?> diff --git a/include/class.dept.php b/include/class.dept.php new file mode 100644 index 0000000000000000000000000000000000000000..8bd575116a421dba88b82dd236243829aeadfc32 --- /dev/null +++ b/include/class.dept.php @@ -0,0 +1,318 @@ +<?php +/********************************************************************* + class.dept.php + + Department class + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Dept { + var $id; + + var $email; + var $sla; + var $manager; + var $ht; + + function Dept($id){ + $this->id=0; + $this->load($id); + } + + function load($id=0) { + global $cfg; + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT dept.*,dept.dept_id as id,dept.dept_name as name, dept.dept_signature as signature, count(staff.staff_id) as users ' + .' FROM '.DEPT_TABLE.' dept ' + .' LEFT JOIN '.STAFF_TABLE.' staff ON (dept.dept_id=staff.dept_id) ' + .' WHERE dept.dept_id='.db_input($id) + .' GROUP BY dept.dept_id'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['dept_id']; + $this->email=$this->sla=$this->manager=null; + $this->getEmail(); //Auto load email struct. + + return true; + } + + function reload(){ + return $this->load(); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['name']; + } + + + function getEmailId(){ + return $this->ht['email_id']; + } + + function getEmail(){ + + if(!$this->email && $this->getEmailId()) + $this->email=Email::lookup($this->getEmailId()); + + return $this->email; + } + + function getNumStaff(){ + return $this->ht['users']; + } + + function getNumMembers(){ + return $this->getNumStaff(); + } + + function getNumUsers(){ + return $this->getNumStaff(); + } + + + function getSLAId(){ + return $this->ht['sla_id']; + } + + function getSLA(){ + + if(!$this->sla && $this->getSLAId()) + $this->sla=SLA::lookup($this->getSLAId()); + + return $this->sla; + } + + function getTemplateId() { + return $this->ht['tpl_id']; + } + + function getTemplate() { + + if(!$this->template && $this->getTemplateId()) + $this->template = Template::lookup($this->getTemplateId()); + + return $this->template; + } + + function getAutoRespEmail() { + + if(!$this->autorespEmail && $this->ht['autoresp_email_id'] && ($email=Email::lookup($this->ht['autoresp_email_id']))) + $this->autorespEmail=$email; + else // Defualt to dept email if autoresp is not specified or deleted. + $this->autorespEmail=$this->getEmail(); + + return $this->autorespEmail; + } + + function getEmailAddress() { + if(($email=$this->getEmail())) + return $email->getAddress(); + } + + function getSignature() { + return $this->ht['signature']; + } + + function canAppendSignature() { + return ($this->getSignature() && $this->isPublic()); + } + + function getManagerId(){ + return $this->ht['manager_id']; + } + + function getManager(){ + + if(!$this->manager && $this->getManagerId()) + $this->manager=Staff::lookup($this->getManagerId()); + + return $this->manager; + } + + function isPublic(){ + return ($this->ht['ispublic']); + } + + function autoRespONNewTicket(){ + return ($this->ht['ticket_auto_response']); + } + + function autoRespONNewMessage(){ + return ($this->ht['message_auto_response']); + } + + function noreplyAutoResp(){ + return ($this->ht['noreply_autoresp']); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo(){ + return $this->getHashtable(); + } + + function update($vars,&$errors){ + + if($this->save($this->getId(),$vars,$errors)) { + $this->reload(); + return true; + } + + return false; + } + + function delete() { + global $cfg; + + if(!$cfg || $this->getId()==$cfg->getDefaultDeptId() || $this->getNumUsers()) + return 0; + + $id=$this->getId(); + $sql='DELETE FROM '.DEPT_TABLE.' WHERE dept_id='.db_input($id).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())){ + // DO SOME HOUSE CLEANING + //Move tickets to default Dept. TODO: Move one ticket at a time and send alerts + log notes. + db_query('UPDATE '.TICKET_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id)); + //Move Dept members: This should never happen..since delete should be issued only to empty Depts...but check it anyways + db_query('UPDATE '.STAFF_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id)); + //make help topic using the dept default to default-dept. + db_query('UPDATE '.TOPIC_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id)); + + } + + return $num; + } + + /*----Static functions-------*/ + function getIdByName($name) { + $id=0; + $sql ='SELECT dept_id FROM '.DEPT_TABLE.' WHERE dept_name='.db_input($name); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($dept = new Dept($id)) && $dept->getId()==$id)?$dept:null; + } + + function getNameById($id) { + + if($id && ($dept=Dept::lookup($id))) + $name= $dept->getName(); + + return $name; + } + + function getDefaultDeptName() { + global $cfg; + return ($cfg && $cfg->getDefaultDeptId() && ($name=Dept::getNameById($cfg->getDefaultDeptId())))?$name:null; + } + + function getDepartments( $publiconly=false) { + + $depts=array(); + $sql ='SELECT dept_id, dept_name FROM '.DEPT_TABLE; + if($publiconly) + $sql.=' WHERE ispublic=1'; + + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id, $name)=db_fetch_row($res)) + $depts[$id] = $name; + } + + return $depts; + } + + function getPublicDepartments() { + return self::getDepartments(true); + } + + function create($vars,&$errors) { + return Dept::save(0,$vars,$errors); + } + + function save($id,$vars,&$errors) { + global $cfg; + + if($id && $id!=$vars['id']) + $errors['err']='Missing or invalid Dept ID (internal error).'; + + if(!$vars['email_id'] || !is_numeric($vars['email_id'])) + $errors['email_id']='Email selection required'; + + if(!is_numeric($vars['tpl_id'])) + $errors['tpl_id']='Template selection required'; + + if(!$vars['name']){ + $errors['name']='Name required'; + }elseif(strlen($vars['name'])<4) { + $errors['name']='Name is too short.'; + }elseif(($did=Dept::getIdByName($vars['name'])) && $did!=$id){ + $errors['name']='Department already exist'; + } + + if(!$vars['ispublic'] && ($vars['id']==$cfg->getDefaultDeptId())) + $errors['ispublic']='System default department can not be private'; + + if($errors) return false; + + + $sql='SET updated=NOW() ' + .' ,ispublic='.db_input($vars['ispublic']) + .' ,email_id='.db_input($vars['email_id']) + .' ,tpl_id='.db_input($vars['tpl_id']) + .' ,sla_id='.db_input($vars['sla_id']) + .' ,autoresp_email_id='.db_input($vars['autoresp_email_id']) + .' ,manager_id='.db_input($vars['manager_id']?$vars['manager_id']:0) + .' ,dept_name='.db_input(Format::striptags($vars['name'])) + .' ,dept_signature='.db_input(Format::striptags($vars['signature'])) + .' ,ticket_auto_response='.db_input(isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1) + .' ,message_auto_response='.db_input(isset($vars['message_auto_response'])?$vars['message_auto_response']:1); + + + if($id) { + $sql='UPDATE '.DEPT_TABLE.' '.$sql.' WHERE dept_id='.db_input($id); + if(db_query($sql) && db_affected_rows()) + return true; + + $errors['err']='Unable to update '.Format::htmlchars($vars['name']).' Dept. Error occurred'; + + }else{ + $sql='INSERT INTO '.DEPT_TABLE.' '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + + $errors['err']='Unable to create department. Internal error'; + + } + + + return false; + } + +} +?> diff --git a/include/class.dispatcher.php b/include/class.dispatcher.php new file mode 100644 index 0000000000000000000000000000000000000000..507b2f9fe3163a30a81e944ebc876639f95407f0 --- /dev/null +++ b/include/class.dispatcher.php @@ -0,0 +1,189 @@ +<?php +/********************************************************************* + class.dispatcher.php + + Dispatcher that will read files with URL lists in them and attempt to + match the URL requested to a function that should be invoked to handle + the request. + + Jared Hancock + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +/** + * URL resolver and dispatcher. It's meant to be quite lightweight, so the + * functions aren't separated + */ +class Dispatcher { + function Dispatcher($file=false) { + $this->urls = array(); + $this->file = $file; + } + + function resolve($url, $args=null) { + if ($this->file) { $this->lazy_load(); } + # Support HTTP method emulation with the _method GET argument + if (isset($_GET['_method'])) { + $_SERVER['REQUEST_METHOD'] = strtoupper($_GET['method']); + unset($_GET['_method']); + } + foreach ($this->urls as $matcher) { + if ($matcher->matches($url)) { + return $matcher->dispatch($url, $args); + } + } + Http::response(400, "URL not supported"); + } + /** + * Returns the url for the given function and arguments (arguments + * aren't declared, but will be handled + */ + function reverse($func) { } + /** + * Add the url to the list of supported URLs + */ + function append($url, $prefix=false) { + if ($prefix) { $url->setPrefix($prefix); } + array_push($this->urls, $url); + } + /** + * Add the urls from another dispatcher onto this one + */ + function extend($dispatcher) { + foreach ($dispatcher->urls as $url) { $this->append($url); } + /* allow inlining / chaining */ return $this; + } + + /* static */ function include_urls($file, $absolute=false, $lazy=true) { + if (!$absolute) { + # Fetch the working path of the caller + $bt = debug_backtrace(); + $file = dirname($bt[0]["file"]) . "/" . $file; + } + if ($lazy) return new Dispatcher($file); + else return (include $file); + } + /** + * The include_urls() method will create a new Dispatcher and set the + * $this->file to where the file to be loaded is located. When this + * dispatcher is first accessed, the file will be loaded. + */ + function lazy_load() { + $this->extend(include $this->file); + $this->file=false; + } +} + +class UrlMatcher { + function UrlMatcher($regex, $func, $args=false, $method=false) { + # Add the slashes for the Perl syntax + $this->regex = "@" . $regex . "@"; + $this->func = $func; + $this->args = ($args) ? $args : array(); + $this->prefix = false; + $this->method = $method; + } + + function setPrefix($prefix) { $this->prefix = $prefix; } + + function matches($url) { + if ($this->method && $_SERVER['REQUEST_METHOD'] != $this->method) { + return false; + } + return preg_match($this->regex, $url, $this->matches) == 1; + } + + function dispatch($url, $prev_args=null) { + # Remove named values from the match array + $this->matches = array_flip(array_intersect( + array_flip($this->matches), range(0,31))); + if (@get_class($this->func) == "Dispatcher") { + # Trim the leading match off the $url and call the + # sub-dispatcher. This will be the case for lines in the URL + # file like + # url("^/blah", Dispatcher::include_urls("blah/urls.conf.php")) + # Also, pass arguments matched so far (if any) to the receiving + # resolve() method by merging the $prev_args into $this->matches + # (excluding $this->matches[0], which is the matched URL at this + # level) + return $this->func->resolve( + substr($url, strlen($this->matches[0])), + array_merge(($prev_args) ? $prev_args : array(), + array_slice($this->matches, 1))); + } else { + # Drop the first item of the matches array (which is the whole + # matched url). Then merge in any initial arguments. + array_shift($this->matches); + # Prepend received arguments (from a parent Dispatcher). This is + # different from the static args, which are postpended + if (is_array($prev_args)) + $args = array_merge($prev_args, $this->matches); + else $args = $this->matches; + # Add in static args specified in the constructor + $args = array_merge($args, $this->args); + # Apply the $prefix given + list($class, $func) = $this->apply_prefix(); + if ($class) { + # Create instance of the class, which is the first item, + # then call the method which is the second item + $func = array(new $class, $func); + } + if (!is_callable($func)) + Http::response(500, + 'Dispatcher compile error. Function not callable'); + return call_user_func_array($func, $args); + } + } + /** + * For the $prefix recieved by the constuctor, prepend it to the + * received $class, if any, then make an import if necessary. Lastly, + * return the appropriate $class, and $func that should be invoked to + * dispatch the URL. + */ + function apply_prefix() { + if (is_array($this->func)) { list($class, $func) = $this->func; } + else { $func = $this->func; $class = ""; } + $class = $this->prefix . $class; + + if (strpos($class, ":")) { + list($file, $class) = explode(":", $class, 2); + include $file; + } + return array($class, $func); + } +} + +function patterns($prefix) { + $disp = new Dispatcher(); + for ($i=1, $k=func_num_args(); $i<$k; $i++) { + # NOTE: that $prefix is added to each url rather than to the + # dispatcher as a whole so that urls can be copied from one + # dispatcher to another (via the ->extend() method) and + # completely maintain their integrity + $disp->append(func_get_arg($i), $prefix); + } + return $disp; +} + +function url($regex, $func, $args=false, $method=false) { + return new UrlMatcher($regex, $func, $args, $method); +} + +function url_post($regex, $func, $args=false) { + return url($regex, $func, $args, "POST"); +} + +function url_get($regex, $func, $args=false) { + return url($regex, $func, $args, "GET"); +} + +function url_del($regex, $func, $args=false) { + return url($regex, $func, $args, "DELETE"); +} +?> diff --git a/include/class.email.php b/include/class.email.php new file mode 100644 index 0000000000000000000000000000000000000000..0b4317f7e6e6dc82866288fc88631ff601b5753f --- /dev/null +++ b/include/class.email.php @@ -0,0 +1,461 @@ +<?php +/********************************************************************* + class.email.php + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +include_once(INCLUDE_DIR.'class.dept.php'); +include_once(INCLUDE_DIR.'class.mailfetch.php'); + +class Email { + var $id; + var $address; + + var $dept; + var $ht; + + function Email($id) { + $this->id=0; + $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT * FROM '.EMAIL_TABLE.' WHERE email_id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['email_id']; + $this->address=$this->ht['name']?($this->ht['name'].'<'.$this->ht['email'].'>'):$this->ht['email']; + + $this->dept = null; + + return true; + } + + function reload() { + return $this->load(); + } + + function getId() { + return $this->id; + } + + function getEmail() { + return $this->ht['email']; + } + + function getAddress() { + return $this->address; + } + + function getName() { + return $this->ht['name']; + } + + function getPriorityId() { + return $this->ht['priority_id']; + } + + function getDeptId() { + return $this->ht['dept_id']; + } + + function getDept() { + + if(!$this->dept && $this->getDeptId()) + $this->dept=Dept::lookup($this->getDeptId()); + + return $this->dept; + } + + function autoRespond() { + return (!$this->ht['noautoresp']); + } + + function getPasswd() { + return $this->ht['userpass']?Mcrypt::decrypt($this->ht['userpass'],SECRET_SALT):''; + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function isSMTPEnabled() { + return $this->ht['smtp_active']; + } + + function allowSpoofing() { + return ($this->ht['smtp_spoofing']); + } + + function getSMTPInfo($active=true) { + $info=array(); + if(!$active || ($active && $this->isSMTPEnabled())) { + + $info = array ('host' => $this->ht['smtp_host'], + 'port' => $this->ht['smtp_port'], + 'auth' => $this->ht['smtp_auth'], + 'username' => $this->ht['userid'], + 'password' =>Mcrypt::decrypt($this->ht['userpass'],SECRET_SALT) + ); + } + + return $info; + } + + function send($to, $subject, $message, $attachments=null, $options=null) { + global $cfg; + + //Get SMTP info IF enabled! + $smtp=array(); + if($this->isSMTPEnabled() && ($info=$this->getSMTPInfo())) { //is SMTP enabled for the current email? + $smtp=$info; + }elseif($cfg && ($email=$cfg->getDefaultSMTPEmail()) && $email->isSMTPEnabled()) { //What about global SMTP setting? + if($email->allowSpoofing() && ($info=$email->getSMTPInfo())) //If spoofing is allowed..then continue. + $smtp=$info; + elseif($email->getId()!=$this->getId()) //No spoofing allowed. Send it via the default SMTP email. + return $email->send($to,$subject,$message,$attachments,$options); + } + + //Get the goodies + require_once ('Mail.php'); // PEAR Mail package + require_once ('Mail/mime.php'); // PEAR Mail_Mime packge + + //do some cleanup + $eol="\n"; + $to=preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); + $subject=stripslashes(preg_replace("/(\r\n|\r|\n)/s",'', trim($subject))); + $body = stripslashes(preg_replace("/(\r\n|\r)/s", "\n", trim($message))); + $fromname=$this->getName(); + $from =sprintf('"%s"<%s>',($fromname?$fromname:$this->getEmail()),$this->getEmail()); + $headers = array ('From' => $from, + 'To' => $to, + 'Subject' => $subject, + 'Date'=>date('D,d M Y H:i:s O'), + 'Message-ID' =>'<'.Misc::randCode(6).''.time().'-'.$this->getEmail().'>', + 'X-Mailer' =>'osTicket v1.7', + 'Content-Type' => 'text/html; charset="UTF-8"' + ); + $mime = new Mail_mime(); + $mime->setTXTBody($body); + //XXX: Attachments + if($attachments){ + foreach($attachments as $attachment) { + if($attachment['file_id'] && ($file=AttachmentFile::lookup($attachment['file_id']))) + $mime->addAttachment($file->getData(),$file->getType(), $file->getName(),false); + elseif($attachment['file'] && file_exists($attachment['file']) && is_readable($attachment['file'])) + $mime->addAttachment($attachment['file'],$attachment['type'],$attachment['name']); + } + } + + $options=array('head_encoding' => 'quoted-printable', + 'text_encoding' => 'quoted-printable', + 'html_encoding' => 'base64', + 'html_charset' => 'utf-8', + 'text_charset' => 'utf-8'); + //encode the body + $body = $mime->get($options); + //encode the headers. + $headers = $mime->headers($headers); + if($smtp) { //Send via SMTP + $mail = mail::factory('smtp', + array ('host' => $smtp['host'], + 'port' => $smtp['port'], + 'auth' => $smtp['auth']?true:false, + 'username' => $smtp['username'], + 'password' => $smtp['password'], + 'timeout' =>20, + 'debug' => false, + )); + $result = $mail->send($to, $headers, $body); + if(!PEAR::isError($result)) + return true; + + $alert=sprintf("Unable to email via %s:%d [%s]\n\n%s\n",$smtp['host'],$smtp['port'],$smtp['username'],$result->getMessage()); + Sys::log(LOG_ALERT,'SMTP Error',$alert,false); + //print_r($result); + } + + //No SMTP or it failed....use php's native mail function. + $mail = mail::factory('mail'); + return PEAR::isError($mail->send($to, $headers, $body))?false:true; + + } + + + function update($vars,&$errors) { + $vars=$vars; + $vars['cpasswd']=$this->getPasswd(); //Current decrypted password. + + if($this->save($this->getId(),$vars,$errors)) { + $this->reload(); + return true; + } + + return false; + } + + + function delete() { + global $cfg; + //Make sure we are not trying to delete default emails. + if(!$cfg || $this->getId()==$cfg->getDefaultEmailId() || $this->getId()==$cfg->getAlertEmailId()) //double...double check. + return 0; + + $sql='DELETE FROM '.EMAIL_TABLE.' WHERE email_id='.db_input($this->getId()).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())) { + $sql='UPDATE '.DEPT_TABLE.' SET autoresp_email_id=0 '. + ',email_id='.db_input($cfg->getDefaultEmailId()). + ' WHERE email_id='.db_input($this->getId()); + db_query($sql); + } + + return $num; + } + + + /******* Static functions ************/ + + //sends emails using native php mail function Email::sendmail( ......); + //Don't use this function if you can help it. + function sendmail($to,$subject,$message,$from) { + + require_once ('Mail.php'); // PEAR Mail package + require_once ('Mail/mime.php'); // PEAR Mail_Mime packge + + $eol="\n"; + $to=preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); + $subject=stripslashes(preg_replace("/(\r\n|\r|\n)/s",'', trim($subject))); + $body = stripslashes(preg_replace("/(\r\n|\r)/s", "\n", trim($message))); + $headers = array ('From' =>$from, + 'To' => $to, + 'Subject' => $subject, + 'Message-ID' =>'<'.Misc::randCode(10).''.time().'@osTicket>', + 'X-Mailer' =>'osTicket v 1.6', + 'Content-Type' => 'text/html; charset="UTF-8"' + ); + $mime = new Mail_mime(); + $mime->setTXTBody($body); + $options=array('head_encoding' => 'quoted-printable', + 'text_encoding' => 'quoted-printable', + 'html_encoding' => 'base64', + 'html_charset' => 'utf-8', + 'text_charset' => 'utf-8'); + //encode the body + $body = $mime->get($options); + //headers + $headers = $mime->headers($headers); + $mail = mail::factory('mail'); + return PEAR::isError($mail->send($to, $headers, $body))?false:true; + } + + + function getIdByEmail($email) { + + $sql='SELECT email_id FROM '.EMAIL_TABLE.' WHERE email='.db_input($email); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($var) { + $id=is_numeric($var)?$var:Email::getIdByEmail($var); + return ($id && is_numeric($id) && ($email=new Email($id)) && $email->getId())?$email:null; + } + + function create($vars,&$errors) { + return Email::save(0,$vars,$errors); + } + + + function save($id,$vars,&$errors) { + global $cfg; + //very basic checks + + $vars['name']=Format::striptags(trim($vars['name'])); + + if($id && $id!=$vars['id']) + $errors['err']='Internal error. Get technical help.'; + + if(!$vars['email'] || !Validator::is_email($vars['email'])) { + $errors['email']='Valid email required'; + }elseif(($eid=Email::getIdByEmail($vars['email'])) && $eid!=$id) { + $errors['email']='Email already exits'; + }elseif($cfg && !strcasecmp($cfg->getAdminEmail(),$vars['email'])) { + $errors['email']='Email already used as admin email!'; + }elseif(Staff::getIdByEmail($vars['email'])) { //make sure the email doesn't belong to any of the staff + $errors['email']='Email in-use by a staff member'; + } + + if(!$vars['name']) + $errors['name']='Email name required'; + + if($vars['mail_active'] || ($vars['smtp_active'] && $vars['smtp_auth'])) { + if(!$vars['userid']) + $errors['userid']='Username missing'; + + if(!$id && !$vars['passwd']) + $errors['passwd']='Password required'; + } + + if($vars['mail_active']) { + //Check pop/imapinfo only when enabled. + if(!function_exists('imap_open')) + $errors['mail_active']= 'IMAP doesn\'t exist. PHP must be compiled with IMAP enabled.'; + if(!$vars['mail_host']) + $errors['mail_host']='Host name required'; + if(!$vars['mail_port']) + $errors['mail_port']='Port required'; + if(!$vars['mail_protocol']) + $errors['mail_protocol']='Select protocol'; + if(!$vars['mail_fetchfreq'] || !is_numeric($vars['mail_fetchfreq'])) + $errors['mail_fetchfreq']='Fetch interval required'; + if(!$vars['mail_fetchmax'] || !is_numeric($vars['mail_fetchmax'])) + $errors['mail_fetchmax']='Maximum emails required'; + if(!$vars['dept_id'] || !is_numeric($vars['dept_id'])) + $errors['dept_id']='You must select a Dept.'; + if(!$vars['priority_id']) + $errors['priority_id']='You must select a priority'; + + if(!isset($vars['postfetch'])) + $errors['postfetch']='Indicate what to do with fetched emails'; + elseif(!strcasecmp($vars['postfetch'],'archive')) { + if(!$vars['mail_archivefolder']) + $errors['postfetch']='Valid folder required'; + } + + } + + if($vars['smtp_active']) { + if(!$vars['smtp_host']) + $errors['smtp_host']='Host name required'; + if(!$vars['smtp_port']) + $errors['smtp_port']='Port required'; + } + + //abort on errors + if($errors) return false; + + if(!$errors && ($vars['mail_host'] && $vars['userid'])) { + $sql='SELECT email_id FROM '.EMAIL_TABLE + .' WHERE mail_host='.db_input($vars['mail_host']).' AND userid='.db_input($vars['userid']); + if($id) + $sql.=' AND email_id!='.db_input($id); + + if(db_num_rows(db_query($sql))) + $errors['userid']=$errors['host']='Host/userid combination already in-use.'; + } + + $passwd=$vars['passwd']?$vars['passwd']:$vars['cpasswd']; + if(!$errors && $vars['mail_active']) { + + //note: password is unencrypted at this point...MailFetcher expect plain text. + $fetcher = new MailFetcher($vars['userid'],$passwd,$vars['mail_host'],$vars['mail_port'], + $vars['mail_protocol'],$vars['mail_encryption']); + if(!$fetcher->connect()) { + $errors['err']='Invalid login. Check '.Format::htmlchars($vars['mail_protocol']).' settings'; + $errors['mail']='<br>'.$fetcher->getLastError(); + }elseif($vars['mail_archivefolder'] && !$fetcher->checkMailbox($vars['mail_archivefolder'],true)) { + $errors['postfetch']='Invalid or unknown mail folder! >> '.$fetcher->getLastError().''; + if(!$errors['mail']) + $errors['mail']='Invalid or unknown archive folder!'; + } + } + + if(!$errors && $vars['smtp_active']) { //Check SMTP login only. + require_once 'Mail.php'; // PEAR Mail package + $smtp = mail::factory('smtp', + array ('host' => $vars['smtp_host'], + 'port' => $vars['smtp_port'], + 'auth' => $vars['smtp_auth']?true:false, + 'username' =>$vars['userid'], + 'password' =>$passwd, + 'timeout' =>20, + 'debug' => false, + )); + $mail = $smtp->connect(); + if(PEAR::isError($mail)) { + $errors['err']='Unable to login. Check SMTP settings.'; + $errors['smtp']='<br>'.$mail->getMessage(); + }else{ + $smtp->disconnect(); //Thank you, sir! + } + } + + if($errors) return false; + + //Default to default priority and dept.. + if(!$vars['priority_id'] && $cfg) + $vars['priority_id']=$cfg->getDefaultPriorityId(); + if(!$vars['dept_id'] && $cfg) + $vars['dept_id']=$cfg->getDefaultDeptId(); + + $sql='updated=NOW(),mail_errors=0, mail_lastfetch=NULL'. + ',email='.db_input($vars['email']). + ',name='.db_input(Format::striptags($vars['name'])). + ',dept_id='.db_input($vars['dept_id']). + ',priority_id='.db_input($vars['priority_id']). + ',noautoresp='.db_input(isset($vars['noautoresp'])?1:0). + ',userid='.db_input($vars['userid']). + ',mail_active='.db_input($vars['mail_active']). + ',mail_host='.db_input($vars['mail_host']). + ',mail_protocol='.db_input($vars['mail_protocol']?$vars['mail_protocol']:'POP'). + ',mail_encryption='.db_input($vars['mail_encryption']). + ',mail_port='.db_input($vars['mail_port']?$vars['mail_port']:0). + ',mail_fetchfreq='.db_input($vars['mail_fetchfreq']?$vars['mail_fetchfreq']:0). + ',mail_fetchmax='.db_input($vars['mail_fetchmax']?$vars['mail_fetchmax']:0). + ',smtp_active='.db_input($vars['smtp_active']). + ',smtp_host='.db_input($vars['smtp_host']). + ',smtp_port='.db_input($vars['smtp_port']?$vars['smtp_port']:0). + ',smtp_auth='.db_input($vars['smtp_auth']). + ',smtp_spoofing='.db_input(isset($vars['smtp_spoofing'])?1:0). + ',notes='.db_input($vars['notes']); + + //Post fetch email handling... + if($vars['postfetch'] && !strcasecmp($vars['postfetch'],'delete')) + $sql.=',mail_delete=1,mail_archivefolder=NULL'; + elseif($vars['postfetch'] && !strcasecmp($vars['postfetch'],'archive') && $vars['mail_archivefolder']) + $sql.=',mail_delete=0,mail_archivefolder='.db_input($vars['mail_archivefolder']); + else + $sql.=',mail_delete=0,mail_archivefolder=NULL'; + + if($vars['passwd']) //New password - encrypt. + $sql.=',userpass='.db_input(Mcrypt::encrypt($vars['passwd'],SECRET_SALT)); + + if($id) { //update + $sql='UPDATE '.EMAIL_TABLE.' SET '.$sql.' WHERE email_id='.db_input($id); + if(db_query($sql) && db_affected_rows()) + return true; + + $errors['err']='Unable to update email. Internal error occurred'; + }else { + $sql='INSERT INTO '.EMAIL_TABLE.' SET '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to add email. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.faq.php b/include/class.faq.php new file mode 100644 index 0000000000000000000000000000000000000000..63b0848e469c7b3b1df6dbb9d61e27fca2c28118 --- /dev/null +++ b/include/class.faq.php @@ -0,0 +1,356 @@ +<?php +/********************************************************************* + class.faq.php + + Backend support for article creates, edits, deletes, and attachments. + + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once('class.file.php'); +require_once('class.category.php'); + +class FAQ { + + var $id; + var $ht; + var $category; + + function FAQ($id) { + + + $this->id=0; + $this->ht = array(); + $this->load($id); + } + + function load($id) { + + $sql='SELECT faq.*,cat.ispublic, count(attach.file_id) as attachments ' + .' FROM '.FAQ_TABLE.' faq ' + .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id) ' + .' LEFT JOIN '.FAQ_ATTACHMENT_TABLE.' attach ON(attach.faq_id=faq.faq_id) ' + .' WHERE faq.faq_id='.db_input($id) + .' GROUP BY faq.faq_id'; + + if (!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht = db_fetch_array($res); + $this->ht['id'] = $this->id = $this->ht['faq_id']; + $this->category = null; + + return true; + } + + function reload() { + return $this->load($this->getId()); + } + + /* ------------------> Getter methods <--------------------- */ + function getId() { return $this->id; } + function getHashtable() { return $this->ht; } + function getKeywords() { return $this->ht['keywords']; } + function getQuestion() { return $this->ht['question']; } + function getAnswer() { return $this->ht['answer']; } + function getNotes() { return $this->ht['notes']; } + function getNumAttachments() { return $this->ht['attachments']; } + + function isPublished() { return (!!$this->ht['ispublished'] && !!$this->ht['ispublic']); } + + function getCreateDate() { return $this->ht['created']; } + function getUpdateDate() { return $this->ht['updated']; } + + function getCategoryId() { return $this->ht['category_id']; } + function getCategory() { + if(!$this->category && $this->getCategoryId()) + $this->category = Category::lookup($this->getCategoryId()); + + return $this->category; + } + + function getHelpTopicsIds() { + + if (!isset($this->ht['topics']) && ($topics=$this->getHelpTopics())) { + $this->ht['topics'] = array_keys($topics); + } + + return $this->ht['topics']; + } + + function getHelpTopics() { + //XXX: change it to obj (when needed)! + + if (!isset($this->topics)) { + $this->topics = array(); + $sql='SELECT t.topic_id, t.topic FROM '.TOPIC_TABLE.' t ' + .' INNER JOIN '.FAQ_TOPIC_TABLE.' ft USING(topic_id) ' + .' WHERE ft.faq_id='.db_input($this->id) + .' ORDER BY t.topic'; + if (($res=db_query($sql)) && db_num_rows($res)) { + while(list($id,$name) = db_fetch_row($res)) + $this->topics[$id]=$name; + } + } + + return $this->topics; + } + + /* ------------------> Setter methods <--------------------- */ + function setPublished($val) { $this->ht['ispublished'] = !!$val; } + function setQuestion($question) { $this->ht['question'] = Format::striptags(trim($question)); } + function setAnswer($text) { $this->ht['answer'] = $text; } + function setKeywords($words) { $this->ht['keywords'] = $words; } + function setNotes($text) { $this->ht['notes'] = $text; } + + /* For ->attach() and ->detach(), use $this->attachments() */ + function attach($file) { return $this->_attachments->add($file); } + function detach($file) { return $this->_attachments->remove($file); } + + function publish() { + $this->setPublished(1); + + return $this->apply(); + } + + function unpublish() { + $this->setPublished(0); + + return $this->apply(); + } + + /* Same as update - but mainly called after one or more setters are changed. */ + function apply() { + //XXX: set errors and add ->getErrors() & ->getError() + return $this->update($this->ht, $errors); + } + + function updateTopics($ids){ + + if($ids) { + $topics = $this->getHelpTopicsIds(); + foreach($ids as $k=>$id) { + if($topics && in_array($id,$topics)) continue; + $sql='INSERT IGNORE INTO '.FAQ_TOPIC_TABLE + .' SET faq_id='.db_input($this->getId()) + .', topic_id='.db_input($id); + db_query($sql); + } + } + + $sql='DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->getId()); + if($ids) + $sql.=' AND topic_id NOT IN('.implode(',',$ids).')'; + + db_query($sql); + + return true; + } + + function update($vars, &$errors) { + + if(!$this->save($this->getId(), $vars, $errors)) + return false; + + $this->updateTopics($vars['topics']); + $this->reload(); + + return true; + } + + + function getAttachments() { + + if(!$this->attachments && $this->getNumAttachments()) { + + $sql='SELECT f.id, f.size, f.hash, f.name ' + .' FROM '.FILE_TABLE.' f ' + .' INNER JOIN '.FAQ_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' + .' WHERE a.faq_id='.db_input($this->getId()); + + $this->attachments = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) { + $rec['key'] =md5($rec['id'].session_id().$rec['hash']); + $this->attachments[] = $rec; + } + } + } + return $this->attachments; + } + + function getAttachmentsLinks($separator=' ',$target='') { + + $str=''; + if(($attachments=$this->getAttachments())) { + foreach($attachments as $attachment ) { + /* The h key must match validation in file.php */ + $hash=$attachment['hash'].md5($attachment['id'].session_id().$attachment['hash']); + if($attachment['size']) + $size=sprintf('(<i>%s</i>)',Format::file_size($attachment['size'])); + + $str.=sprintf('<a class="Icon file" href="file.php?h=%s" target="%s">%s</a>%s %s', + $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + + } + } + return $str; + } + + function uploadAttachments($files) { + + foreach($files as $file) { + if(($fileId=is_numeric($file)?$file:AttachmentFile::upload($file)) && is_numeric($fileId)) { + $sql ='INSERT INTO '.FAQ_ATTACHMENT_TABLE + .' SET faq_id='.db_input($this->getId()).', file_id='.db_input($fileId); + if(db_query($sql)) $i++; + } + } + + if($i) $this->reload(); + + return $i; + } + + function deleteAttachment($fileId) { + + $sql='DELETE FROM '.FAQ_ATTACHMENT_TABLE + .' WHERE faq_id='.db_input($this->getId()) + .' AND file_id='.db_input($fileId) + .' LIMIT 1'; + + if(!db_query($sql) || !db_affected_rows()) + return false; + + if(($file=AttachmentFile::lookup($fileId)) && !$file->isInuse()) + $file->delete(); + + return true; + } + + function deleteAttachments(){ + + $deleted=0; + if(($attachments = $this->getAttachments())) { + foreach($attachments as $attachment) + if($attachment['id'] && $this->deleteAttachment($attachment['id'])) + $deleted++; + } + + return $deleted; + } + + + function delete() { + + $sql='DELETE FROM '.FAQ_TABLE + .' WHERE faq_id='.db_input($this->getId()) + .' LIMIT 1'; + if(!db_query($sql) || !db_affected_rows()) + return false; + + //Cleanup help topics. + db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->id)); + //Cleanup attachments. + $this->deleteAttachments(); + + return true; + } + + /* ------------------> Static methods <--------------------- */ + + function add($vars, &$errors) { + if(($id=self::create($vars, $errors)) && ($faq=self::lookup($id))) + $faq->updateTopics($vars['topics']); + + return$faq; + } + + function create($vars, &$errors) { + return self::save(0, $vars, $errors); + } + + function lookup($id) { + return ($id && is_numeric($id) && ($obj= new FAQ($id)) && $obj->getId()==$id)? $obj : null; + } + + function countPublishedFAQs() { + $sql='SELECT count(faq.faq_id) ' + .' FROM '.FAQ_TABLE.' faq ' + .' INNER JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id AND cat.ispublic=1) ' + .' WHERE faq.ispublished=1'; + + return db_result(db_query($sql)); + } + + function findIdByQuestion($question) { + $sql='SELECT faq_id FROM '.FAQ_TABLE + .' WHERE question='.db_input($question); + + list($id) =db_fetch_row(db_query($sql)); + + return $id; + } + + function findByQuestion($question) { + + if(($id=self::getIdByQuestion($question))) + return self::lookup($id); + + return false; + } + + function save($id, $vars, &$errors, $validation=false) { + + //Cleanup. + $vars['question']=Format::striptags(trim($vars['question'])); + + //validate + if($id && $id!=$vars['id']) + $errors['err'] = 'Internal error. Try again'; + + if(!$vars['question']) + $errors['question'] = 'Question required'; + elseif(($qid=self::findIdByQuestion($vars['question'])) && $qid!=$id) + $errors['question'] = 'Question already exists'; + + if(!$vars['category_id'] || !($category=Category::lookup($vars['category_id']))) + $errors['category_id'] = 'Category is required'; + + if(!$vars['answer']) + $errors['answer'] = 'FAQ answer is required'; + + if($errors || $validation) return (!$errors); + + //save + $sql=' updated=NOW() ' + .', question='.db_input($vars['question']) + .', answer='.db_input(Format::safe_html($vars['answer'])) + .', category_id='.db_input($vars['category_id']) + .', ispublished='.db_input(isset($vars['ispublished'])?$vars['ispublished']:0) + .', notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.FAQ_TABLE.' SET '.$sql.' WHERE faq_id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update FAQ.'; + + } else { + $sql='INSERT INTO '.FAQ_TABLE.' SET '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create FAQ. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.file.php b/include/class.file.php new file mode 100644 index 0000000000000000000000000000000000000000..af38bfd59e43b45471dc35d8e1a256678c05588a --- /dev/null +++ b/include/class.file.php @@ -0,0 +1,234 @@ +<?php +/********************************************************************* + class.file.php + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class AttachmentFile { + + var $id; + var $ht; + + function AttachmentFile($id) { + $this->id =0; + return ($this->load($id)); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT f.*, count(DISTINCT c.canned_id) as canned, count(DISTINCT t.ticket_id) as tickets ' + .' FROM '.FILE_TABLE.' f ' + .' LEFT JOIN '.CANNED_ATTACHMENT_TABLE.' c ON(c.file_id=f.id) ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' t ON(t.file_id=f.id) ' + .' WHERE f.id='.db_input($id) + .' GROUP BY f.id'; + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id =$this->ht['id']; + + return true; + } + + function reload() { + return $this->load(); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function getNumTickets() { + return $this->ht['tickets']; + } + + function isCanned() { + return ($this->ht['canned']); + } + + function isInUse() { + return ($this->getNumTickets() || $this->isCanned()); + } + + function getId() { + return $this->id; + } + + function getType() { + return $this->ht['type']; + } + + function getMime() { + return $this->getType(); + } + + function getSize() { + return $this->ht['size']; + } + + function getName() { + return $this->ht['name']; + } + + function getHash() { + return $this->ht['hash']; + } + + function getBinary() { + return $this->ht['filedata']; + } + + function getData() { + return $this->getBinary(); + } + + function delete() { + + $sql='DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1'; + return (db_query($sql) && db_affected_rows()); + } + + + function display() { + + + header('Content-type: '.$this->getType()?$this->getType():'application/octet-stream'); + header('Content-Length: '.$this->getSize()); + echo $this->getData(); + exit(); + } + + function download() { + + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: public'); + header('Content-Type: application/octet-stream'); + + //header('Content-Type: '.$this->getType()?$this->getType():'application/octet-stream'); + + $filename=basename($this->getName()); + $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']); + if ((is_integer(strpos($user_agent,'msie'))) && (is_integer(strpos($user_agent,'win')))) { + header('Content-Disposition: filename='.$filename.';'); + }else{ + header('Content-Disposition: attachment; filename='.$filename.';' ); + } + + header('Content-Transfer-Encoding: binary'); + header('Content-Length: '.$this->getSize()); + echo $this->getBinary(); + exit(); + } + + function upload($file) { + + if(!$file['name'] || !is_uploaded_file($file['tmp_name'])) + return false; + + $info=array('type'=>$file['type'], + 'size'=>$file['size'], + 'name'=>$file['name'], + 'hash'=>MD5(MD5_FILE($file['tmp_name']).time()), + 'data'=>file_get_contents($file['tmp_name']) + ); + + return AttachmentFile::save($info); + } + + function save($file) { + + if(!$file['hash']) + $file['hash']=MD5(MD5($file['data']).time()); + if(!$file['size']) + $file['size']=strlen($file['data']); + + + + //TODO: Do chunked INSERTs - + if(($mps=db_get_variable('max_allowed_packet')) && $file['size']>($mps*0.7)) { + @db_set_variable('max_allowed_packet',$file['size']+$mps); + } + + $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() ' + .',type='.db_input($file['type']) + .',size='.db_input($file['size']) + .',name='.db_input($file['name']) + .',hash='.db_input($file['hash']) + .',filedata='.db_input($file['data']); + + return db_query($sql)?db_insert_id():0; + } + + /* Static functions */ + function getIdByHash($hash) { + + $sql='SELECT id FROM '.FILE_TABLE.' WHERE hash='.db_input($hash); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id) { + + $id = is_numeric($id)?$id:AttachmentFile::getIdByHash($id); + + return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null; + } +} + +class AttachmentList { + function AttachmentList($table, $key) { + $this->table = $table; + $this->key = $key; + } + + function all() { + if (!isset($this->list)) { + $this->list = array(); + $res=db_query('SELECT file_id FROM '.$this->table + .' WHERE '.$this->key); + while(list($id) = db_fetch_row($res)) { + $this->list[] = new AttachmentFile($id); + } + } + return $this->list; + } + + function getCount() { + return count($this->all()); + } + + function add($fileId) { + db_query( + 'INSERT INTO '.$this->table + .' SET '.$this->key + .' file_id='.db_input($fileId)); + } + + function remove($fileId) { + db_query( + 'DELETE FROM '.$this->table + .' WHERE '.$this->key + .' AND file_id='.db_input($fileId)); + } +} +?> diff --git a/include/class.filter.php b/include/class.filter.php new file mode 100644 index 0000000000000000000000000000000000000000..9a1caf2b1b0792c9cea14d9cbf28cf4a458ea0ce --- /dev/null +++ b/include/class.filter.php @@ -0,0 +1,756 @@ +<?php +/********************************************************************* + class.filter.php + + Email Filter Class + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Filter { + + var $id; + var $ht; + + function Filter($id){ + $this->id=0; + $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT filter.*,count(rule.id) as rule_count ' + .' FROM '.EMAIL_FILTER_TABLE.' filter ' + .' LEFT JOIN '.EMAIL_FILTER_RULE_TABLE.' rule ON(rule.filter_id=filter.id) ' + .' WHERE filter.id='.db_input($id) + .' GROUP BY filter.id'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id=$info['id']; + + return true; + } + + function reload() { + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['name']; + } + + function getNotes(){ + return $this->ht['notes']; + } + + function getInfo(){ + return $this->ht; + } + + function getNumRules(){ + return $this->ht['rule_count']; + } + + function getExecOrder(){ + return $this->ht['execorder']; + } + + function isActive(){ + return ($this->ht['isactive']); + } + + function isSystemBanlist() { + return !strcasecmp($this->getName(),'SYSTEM BAN LIST'); + } + + function getDeptId(){ + return $this->ht['dept_id']; + } + + function getPriorityId(){ + return $this->ht['priority_id']; + } + + function getSLAId(){ + return $this->ht['sla_id']; + } + + function getStaffId(){ + return $this->ht['staff_id']; + } + + function getTeamId(){ + return $this->ht['team_id']; + } + + function stopOnMatch(){ + return ($this->ht['stop_on_match']); + } + + function matchAllRules(){ + return ($this->ht['match_all_rules']); + } + + function rejectEmail(){ + return ($this->ht['reject_email']); + } + + function useReplyToEmail(){ + return ($this->ht['use_replyto_email']); + } + + function disableAlerts(){ + return ($this->ht['disable_autoresponder']); + } + + function sendAlerts(){ + return (!$this->disableAlerts()); + } + + function getRules(){ + if (!$this->ht['rules']) { + $rules=array(); + //We're getting the rules...live because it gets cleared on update. + $sql='SELECT * FROM '.EMAIL_FILTER_RULE_TABLE.' WHERE filter_id='.db_input($this->getId()); + if(($res=db_query($sql)) && db_num_rows($res)){ + while($row=db_fetch_array($res)) + $rules[]=array('w'=>$row['what'],'h'=>$row['how'],'v'=>$row['val']); + } + $this->ht['rules'] = $rules; + } + return $this->ht['rules']; + } + + function getFlatRules(){ //Format used on html... I'm ashamed + + $info=array(); + if(($rules=$this->getRules())){ + foreach($rules as $k=>$rule){ + $i=$k+1; + $info["rule_w$i"]=$rule['w']; + $info["rule_h$i"]=$rule['h']; + $info["rule_v$i"]=$rule['v']; + } + } + return $info; + } + + function addRule($what, $how, $val,$extra=array()) { + + $rule= array_merge($extra,array('w'=>$what, 'h'=>$how, 'v'=>$val)); + $rule['filter_id']=$this->getId(); + + return FilterRule::create($rule,$errors); + } + + function removeRule($what, $how, $val) { + + $sql='DELETE FROM '.EMAIL_FILTER_RULE_TABLE + .' WHERE filter_id='.db_input($this->getId()) + .' AND what='.db_input($what) + .' AND how='.db_input($how) + .' AND val='.db_input($val); + + return (db_query($sql) && db_affected_rows()); + } + + function getRule($id) { + return $this->getRuleById($id); + } + + function getRuleById($id) { + return FilterRule::lookup($id,$this->getId()); + } + + function containsRule($what, $how, $val) { + if (isset($this->ht['rules'])) { + foreach ($this->ht['rules'] as $rule) { + if (array("w"=>$what, "h"=>$how, "v"=>$val) == $rule) { + return True; + } + } + return False; + } else { + # Fetch from database + return 0 != db_count( + "SELECT COUNT(*) FROM ".EMAIL_FILTER_RULE_TABLE + ." WHERE filter_id=".db_input($this->id) + ." AND what=".db_input($what)." AND how=".db_input($how) + ." AND val=".db_input($val) + ); + } + } + /** + * Simple true/false if the rules defined for this filter match the + * incoming email + * + * $email is an ARRAY, which has valid keys + * *from - email address of sender + * name - name of sender + * subject - subject line of the email + * body - body content of the email (no attachments, please) + * reply-to - reply-to email address + * reply-to-name - name of sender to reply-to + * headers - array of email headers + * emailid - osTicket email id of recipient + */ + function matches($email) { + $what = array( + "email" => $email['from'], + "subject" => $email['subject'], + # XXX: Support reply-to too ? + "name" => $email['name'], + "body" => $email['body'] + # XXX: Support headers + ); + $how = array( + # how => array(function, null or === this, null or !== this) + "equal" => array("strcmp", 0), + "not_equal" => array("strcmp", null, 0), + "contains" => array("strpos", null, false), + "dn_contain"=> array("strpos", false) + ); + $match = false; + foreach ($this->getRules() as $rule) { + list($func, $pos, $neg) = $how[$rule['h']]; + # TODO: convert $what and $rule['v'] to mb_strtoupper and do + # case-sensitive, binary-safe comparisons. Would be really + # nice to do $rule['v'] on the database side for + # performance -- but ::getFlatRules() is a blocker + $result = call_user_func($func, strtoupper($what[$rule['w']]), + strtoupper($rule['v'])); + if (($pos === null && $result !== $neg) or ($result === $pos)) { + # Match. + $match = true; + if (!$this->matchAllRules()) break; + } else { + # No match. Continue? + if ($this->matchAllRules()) { + $match = false; + break; + } + } + } + return $match; + } + /** + * If the matches() method returns TRUE, send the initial ticket to this + * method to apply the filter actions defined + */ + function apply(&$ticket, $email=null) { + # TODO: Disable alerting + # XXX: Does this imply turning it on as well? (via ->sendAlerts()) + if ($this->disableAlerts()) $ticket['autorespond']=false; + # Set owning department (?) + if ($this->getDeptId()) $ticket['deptId']=$this->getDeptId(); + # Set ticket priority (?) + if ($this->getPriorityId()) $ticket['pri']=$this->getPriorityId(); + # Set SLA plan (?) + if ($this->getSLAId()) $ticket['slaId']=$this->getSLAId(); + # Auto-assign to (?) + # XXX: Unset the other (of staffId or teamId) (?) + if ($this->getStaffId()) $ticket['staffId']=$this->getStaffId(); + elseif ($this->getTeamId()) $ticket['teamId']=$this->getTeamId(); + # Override name with reply-to information from the EmailFilter + # match + if ($this->useReplyToEmail() && $email['reply-to']) { + $ticket['email'] = $email['reply-to']; + if ($email['reply-to-name']) + $ticket['name'] = $email['reply-to-name']; + } + } + + function update($vars,&$errors){ + + if(!Filter::save($this->getId(),$vars,$errors)) + return false; + + $this->reload(); + + return true; + } + + function delete(){ + + $id=$this->getId(); + $sql='DELETE FROM '.EMAIL_FILTER_TABLE.' WHERE id='.db_input($id).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())){ + db_query('DELETE FROM '.EMAIL_FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id)); + } + + return $num; + } + + /** static functions **/ + function create($vars,&$errors){ + return Filter::save(0,$vars,$errors); + } + + function getIdByName($name){ + + $sql='SELECT id FROM '.EMAIL_FILTER_TABLE.' WHERE name='.db_input($name); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($f= new Filter($id)) && $f->getId()==$id)?$f:null; + } + + function validate_rules($vars,&$errors){ + return self::save_rules(0,$vars,$errors); + } + + function save_rules($id,$vars,&$errors){ + + $matches=array('name','email','subject','body','header'); + $types=array('equal','not_equal','contains','dn_contain'); + + $rules=array(); + for($i=1; $i<=25; $i++) { //Expecting no more than 25 rules... + if($vars["rule_w$i"] || $vars["rule_h$i"]){ + if(!$vars["rule_w$i"] || !in_array($vars["rule_w$i"],$matches)) + $errors["rule_$i"]='Invalid match selection'; + elseif(!$vars["rule_h$i"] || !in_array($vars["rule_h$i"],$types)) + $errors["rule_$i"]='Invalid match type selection'; + elseif(!$vars["rule_v$i"]) + $errors["rule_$i"]='Value required'; + elseif($vars["rule_w$i"]=='email' && $vars["rule_h$i"]=='equal' && !Validator::is_email($vars["rule_v$i"])) + $errors["rule_$i"]='Valid email required for the match type'; + else //for everything-else...we assume it's valid. + $rules[]=array('w'=>$vars["rule_w$i"],'h'=>$vars["rule_h$i"],'v'=>$vars["rule_v$i"]); + }elseif($vars["rule_v$i"]){ + $errors["rule_$i"]='Incomplete selection'; + } + } + + if(!$rules && is_array($vars["rules"])) + # XXX: Validation bypass + $rules = $vars["rules"]; + elseif(!$rules && !$errors) + $errors['rules']='You must set at least one rule.'; + + if($errors) return false; + + if(!$id) return true; //When ID is 0 then assume it was just validation... + + //Clear existing rules...we're doing mass replace on each save!! + db_query('DELETE FROM '.EMAIL_FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id)); + $num=0; + foreach($rules as $rule) { + $rule['filter_id']=$id; + if(FilterRule::create($rule, $errors)) + $num++; + } + + return $num; + } + + function save($id,$vars,&$errors){ + + + if(!$vars['execorder']) + $errors['execorder']='Order required'; + elseif(!is_numeric($vars['execorder'])) + $errors['execorder']='Must be numeric value'; + + if(!$vars['name']) + $errors['name']='Name required'; + elseif(($sid=self::getIdByName($vars['name'])) && $sid!=$id) + $errors['name']='Name already in-use'; + + if(!$errors && !self::validate_rules($vars,$errors) && !$errors['rules']) + $errors['rules']='Unable to validate rules as entered'; + + if($errors) return false; + + $sql=' updated=NOW() '. + ',isactive='.db_input($vars['isactive']). + ',name='.db_input($vars['name']). + ',execorder='.db_input($vars['execorder']). + ',email_id='.db_input($vars['email_id']). + ',dept_id='.db_input($vars['dept_id']). + ',priority_id='.db_input($vars['priority_id']). + ',sla_id='.db_input($vars['sla_id']). + ',match_all_rules='.db_input($vars['match_all_rules']). + ',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0). + ',reject_email='.db_input(isset($vars['reject_email'])?1:0). + ',use_replyto_email='.db_input(isset($vars['use_replyto_email'])?1:0). + ',disable_autoresponder='.db_input(isset($vars['disable_autoresponder'])?1:0). + ',notes='.db_input($vars['notes']); + + + //Auto assign ID is overloaded... + if($vars['assign'] && $vars['assign'][0]=='s') + $sql.=',team_id=0,staff_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign'])); + elseif($vars['assign'] && $vars['assign'][0]=='t') + $sql.=',staff_id=0,team_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign'])); + else + $sql.=',staff_id=0,team_id=0 '; //no auto-assignment! + + if($id) { + $sql='UPDATE '.EMAIL_FILTER_TABLE.' SET '.$sql.' WHERE id='.db_input($id); + if(!db_query($sql)) + $errors['err']='Unable to update the filter. Internal error occurred'; + }else{ + $sql='INSERT INTO '.EMAIL_FILTER_TABLE.' SET '.$sql.',created=NOW() '; + if(!db_query($sql) || !($id=db_insert_id())) + $errors['err']='Unable to add filter. Internal error'; + } + + if($errors || !$id) return false; + + //Success with update/create...save the rules. We can't recover from any errors at this point. + self::save_rules($id,$vars,$xerrors); + + return true; + } +} + +class FilterRule { + + var $id; + var $ht; + + var $filter; + + function FilterRule($id,$filterId=0){ + $this->id=0; + $this->load($id,$filterId); + } + + function load($id,$filterId=0) { + + $sql='SELECT rule.* FROM '.EMAIL_FILTER_RULE_TABLE.' rule ' + .' WHERE rule.id='.db_input($id); + if($filterId) + $sql.=' AND rule.filter_id='.db_input($filterId); + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['id']; + + $this->filter=null; + + return true; + } + + function reload() { + return $this->load($this->getId()); + } + + function getId() { + return $this->id; + } + + function isActive() { + return ($this->ht['isactive']); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function getFilterId() { + return $this->ht['filter_id']; + } + + function getFilter() { + + if(!$this->filter && $this->getFilterId()) + $this->filter = Filter::lookup($this->getFilterId()); + + return $this->filter; + } + + function update($vars,&$errors) { + if(!$this->save($this->getId(),$vars,$errors)) + return false; + + $this->reload(); + return true; + } + + function delete(){ + + $sql='DELETE FROM '.EMAIL_FILTER_RULE_TABLE.' WHERE id='.db_input($this->getId()).' AND filter_id='.db_input($this->getFilterId()); + + return (db_query($sql) && db_affected_rows()); + } + + /* static */ function create($vars,&$errors) { + return self::save(0,$vars,$errors); + } + + /* static private */ function save($id,$vars,&$errors) { + + if(!$vars['filter_id']) + $errors['err']='Parent filter ID required'; + + + if($errors) return false; + + $sql=' updated=NOW() '. + ',what='.db_input($vars['w']). + ',how='.db_input($vars['h']). + ',val='.db_input($vars['v']). + ',isactive='.db_input(isset($vars['isactive'])?$vars['isactive']:1); + + + if(isset($vars['notes'])) + $sql.=',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.EMAIL_FILTER_RULE_TABLE.' SET '.$sql.' WHERE id='.db_input($id).' AND filter_id='.db_input($vars['filter_id']); + if(db_query($sql)) + return true; + + } else { + $sql='INSERT INTO '.EMAIL_FILTER_RULE_TABLE.' SET created=NOW(), filter_id='.db_input($vars['filter_id']).', '.$sql; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + } + + return false; + } + + /* static */ function lookup($id,$filterId=0) { + return ($id && is_numeric($id) && ($r= new FilterRule($id,$filterId)) && $r->getId()==$id)?$r:null; + } + +} + +/** + * Applies rules defined in the staff control panel "Email Filters". Each + * filter can have up to 25 rules (*currently). This will attempt to match + * the incoming email against the defined rules, and, if the email matches, + * the ticket will be modified as described in the filter + */ +class EmailFilter { + /** + * Construct a list of filters to handle a new ticket generated from an + * email or something with information common to email (such as API + * calls, etc). + * + * $email is an ARRAY, which has valid keys + * *from - email address of sender + * name - name of sender + * subject - subject line of the email + * email-id - id of osTicket email recipient address + * --------------- + * @see Filter::matches() for a complete list of supported keys + * + * $slow - if TRUE, every (active) filter will be fetched from the + * database and matched against the email. Otherwise, a subset + * of filters from the database that appear to have rules that + * deal with the data in the email will be considered. @see + * ::quickList() for more information. + */ + function EmailFilter($email, $slow=false) { + $this->email = $email; + if ($slow) { + $this->build($this->getAllActive()); + } else { + $this->build( + $this->quickList($email['from'], $email['name'], + $email['subject'])); + } + } + + function build($res) { + $this->filters = array(); + while (list($id) = db_fetch_row($res)) + array_push($this->filters, new Filter($id)); + return $this->filters; + } + /** + * Determine if any filters match the received email, and if so, apply + * actions defined in those filters to the ticket-to-be-created. + */ + function apply(&$ticket) { + foreach ($this->filters as $filter) { + if ($filter->matches($this->email)) { + $filter->apply($ticket, $this->email); + if ($filter->stopOnMatch()) break; + } + } + } + + /* static */ function getAllActive() { + $sql="SELECT id FROM ".EMAIL_FILTER_TABLE." WHERE isactive" + ." ORDER BY execorder"; + + return db_query($sql); + } + /** + * Fast lookup function to all filters that have at least one rule that + * matches the received address or name or is not defined to match based + * on an email-address or sender-name. This method is meant to retrieve + * all possible filters that could potentially match the given + * arguments. This method will request the database to make a first pass + * and eliminate the filters from being considered that would never + * match the received email. + * + * Returns an array<Filter::Id> which will need to have their respective + * matches() method queried to determine if the Filter actually matches + * the email. + * + * -----> Disclaimer <------------------ + * It would seem that this would not work; however, bear in mind that + * this logic is completely backwards from the database design. Rather + * than determining if the email matches the rules, we're determining if + * the rules *might* apply to the email. This is a "quick" method, + * because it does not request the database to fully verify that the + * rule matches the email. Nor does it fetch the rule or filter + * information from the database. Whether the filter will completely + * match or not is determined in the Filter::matches() method. + */ + /* static */ function quickList($addr, $name=false, $subj=false, + $emailid=0) { + $sql="SELECT DISTINCT filter_id FROM ".EMAIL_FILTER_RULE_TABLE." rule" + ." INNER JOIN ".EMAIL_FILTER_TABLE." filter" + ." ON (filter.id=rule.filter_id)" + ." WHERE filter.isactive"; + # Filter by recipient email-id if specified + if ($emailid) #TODO: Fix the logic here... + $sql.=" AND filter.email_id=".db_input($emailid); + # Include rules for sender-email, sender-name and subject as + # requested + $sql.=" AND ((what='email' AND LOCATE(val,".db_input($addr)."))"; + if ($name) + $sql.=" OR (what='name' AND LOCATE(val,".db_input($name)."))"; + if ($subj) + $sql.=" OR (what='subject' AND LOCATE(val,".db_input($subj)."))"; + # Also include filters that do not have any rules concerning either + # sender-email-addresses or sender-names or subjects + $sql.=") OR filter.id IN (" + ." SELECT filter_id " + ." FROM ".EMAIL_FILTER_RULE_TABLE." rule" + ." INNER JOIN ".EMAIL_FILTER_TABLE." filter" + ." ON (rule.filter_id=filter.id)" + ." GROUP BY filter_id" + ." HAVING COUNT(*)-COUNT(NULLIF(what,'email'))=0"; + if ($name!==false) $sql.=" AND COUNT(*)-COUNT(NULLIF(what,'name'))=0"; + if ($subj!==false) $sql.=" AND COUNT(*)-COUNT(NULLIF(what,'subject'))=0"; + # Also include filters that do not have match_all_rules set to and + # have at least one rule 'what' type that wasn't considered + $sql.=") OR filter.id IN (" + ." SELECT filter_id" + ." FROM ".EMAIL_FILTER_RULE_TABLE." rule" + ." INNER JOIN ".EMAIL_FILTER_TABLE." filter" + ." ON (rule.filter_id=filter.id)" + ." WHERE what NOT IN ('email'" + # Handle sender-name and subject if specified + .(($name!==false)?",'name'":"") + .(($subj!==false)?",'subject'":"") + .") AND filter.match_all_rules = false" + # Return filters in declared execution order + .") ORDER BY filter.execorder"; + + return db_query($sql); + } + /** + * Quick function to determine if the received email-address is + * indicated by an active email filter to be banned. Returns the id of + * the filter that has the address blacklisted and FALSE if the email is + * not blacklisted. + * + * XXX: If more detailed matching is to be supported, perhaps this + * should receive an array like the constructor and + * Filter::matches() method. + * Peter - Let's keep it as a quick scan for obviously banned emails. + */ + /* static */ function isBanned($addr) { + + $sql='SELECT filter.id, what, how, UPPER(val) ' + .' FROM '.EMAIL_FILTER_TABLE.' filter' + .' INNER JOIN '.EMAIL_FILTER_RULE_TABLE.' rule' + .' ON (filter.id=rule.filter_id)' + .' WHERE filter.reject_email' + .' AND filter.match_all_rules=0' + .' AND filter.email_id=0' + .' AND filter.isactive' + .' AND rule.isactive ' + .' AND rule.what="email"' + .' AND LOCATE(rule.val,'.db_input($addr).')'; + + # XXX: Use MB_xxx function for proper unicode support + $addr = strtoupper($addr); + $how=array('equal' => array('strcmp', 0), + 'contains' => array('strpos', null, false)); + + if ($res=db_query($sql)) { + while ($row=db_fetch_array($res)) { + list($func, $pos, $neg) = $how[$row['how']]; + if (!$func) continue; + $res = call_user_func($func, $addr, $row['val']); + if (($neg === null && $res === $pos) || $res !== $neg) + return $row['id']; + } + } + return false; + } + + /** + * Simple true/false if the headers of the email indicate that the email + * is an automatic response. + * + * Thanks to http://wiki.exim.org/EximAutoReply + * X-Auto-Response-Supress is outlined here, + * http://msdn.microsoft.com/en-us/library/ee219609(v=exchg.80).aspx + */ + /* static */ function isAutoResponse($headers) { + $auto_headers = array( + 'Auto-Submitted' => 'AUTO-REPLIED', + 'Precedence' => array('AUTO_REPLY', 'BULK', 'JUNK', 'LIST'), + 'Subject' => array('OUT OF OFFICE', 'AUTO-REPLY:', 'AUTORESPONSE'), + 'X-Autoreply' => 'YES', + 'X-Auto-Response-Suppress' => 'OOF', + 'X-Autoresponse' => '', + 'X-Auto-Reply-From' => '' + ); + foreach ($auto_headers as $header=>$find) { + if ($value = strtoupper($headers[$header])) { + # Search text must be found at the beginning of the header + # value. This is especially import for something like the + # subject line, where something like an autoreponse may + # appear somewhere else in the value. + if (is_array($find)) { + foreach ($find as $f) + if (strpos($value, $f) === 0) + return true; + } elseif (strpos($value, $find) === 0) { + return true; + } + } + } + return false; + } +} +?> diff --git a/include/class.format.php b/include/class.format.php new file mode 100644 index 0000000000000000000000000000000000000000..0dbd9e1432215d57b92661e99acfe761fa96b522 --- /dev/null +++ b/include/class.format.php @@ -0,0 +1,211 @@ +<?php +/********************************************************************* + class.format.php + + Collection of helper function used for formatting + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + + +class Format { + + + function file_size($bytes) { + + if(!is_numeric($bytes)) + return $bytes; + if($bytes<1024) + return $bytes.' bytes'; + if($bytes <102400) + return round(($bytes/1024),1).' kb'; + + return round(($bytes/1024000),1).' mb'; + } + + function file_name($filename) { + + $search = array('/ß/','/ä/','/Ä/','/ö/','/Ö/','/ü/','/Ü/','([^[:alnum:]._])'); + $replace = array('ss','ae','Ae','oe','Oe','ue','Ue','_'); + return preg_replace($search,$replace,$filename); + } + + /* re-arrange $_FILES array for the sane */ + function files($files) { + + foreach($files as $k => $a) { + if(is_array($a)) + foreach($a as $i => $v) + $result[$i][$k] = $v; + } + + return $result?array_filter($result):$files; + } + + function phone($phone) { + + $stripped= preg_replace("/[^0-9]/", "", $phone); + if(strlen($stripped) == 7) + return preg_replace("/([0-9]{3})([0-9]{4})/", "$1-$2",$stripped); + elseif(strlen($stripped) == 10) + return preg_replace("/([0-9]{3})([0-9]{3})([0-9]{4})/", "($1) $2-$3",$stripped); + else + return $phone; + } + + function truncate($string,$len,$hard=false) { + + if(!$len || $len>strlen($string)) + return $string; + + $string = substr($string,0,$len); + + return $hard?$string:(substr($string,0,strrpos($string,' ')).' ...'); + } + + function strip_slashes($var) { + return is_array($var)?array_map(array('Format','strip_slashes'),$var):stripslashes($var); + } + + function wrap($text,$len=75) { + return wordwrap($text,$len,"\n",true); + } + + function html($html, $config=array('balance'=>1)) { + require_once(INCLUDE_DIR.'htmLawed.php'); + return htmLawed($html, $config); + } + + function safe_html($html) { + return Format::html($html,array('safe'=>1,'balance'=>1)); + } + + function htmlchars($var) { + return is_array($var)?array_map(array('Format','htmlchars'),$var):htmlspecialchars($var,ENT_QUOTES); + } + + function input($var) { + return Format::htmlchars($var); + } + + //Format text for display.. + function display($text) { + global $cfg; + + $text=Format::htmlchars($text); //take care of html special chars + if($cfg && $cfg->clickableURLS() && $text) + $text=Format::clickableurls($text); + + //Wrap long words... + $text=preg_replace_callback('/\w{75,}/',create_function('$matches','return wordwrap($matches[0],70,"\n",true);'),$text); + + return nl2br($text); + } + + function striptags($var) { + return is_array($var)?array_map(array('Format','striptags'),$var):strip_tags(html_entity_decode($var)); //strip all tags ...no mercy! + } + + //make urls clickable. Mainly for display + function clickableurls($text) { + + //Not perfect but it works - please help improve it. + $text=preg_replace('/(((f|ht){1}tp(s?):\/\/)[-a-zA-Z0-9@:%_\+.~#?&;\/\/=]+)/','<a href="\\1" target="_blank">\\1</a>', $text); + $text=preg_replace("/(^|[ \\n\\r\\t])(www\.([a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+)(\/[^\/ \\n\\r]*)*)/", + '\\1<a href="http://\\2" target="_blank">\\2</a>', $text); + $text=preg_replace("/(^|[ \\n\\r\\t])([_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4})/",'\\1<a href="mailto:\\2" target="_blank">\\2</a>', $text); + + return $text; + } + + function stripEmptyLines ($string) { + //return preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $string); + //return preg_replace('/\s\s+/',"\n",$string); //Too strict?? + return preg_replace("/\n{3,}/", "\n\n", $string); + } + + + function linebreaks($string) { + return urldecode(ereg_replace("%0D", " ", urlencode($string))); + } + + + /** + * Thanks, http://us2.php.net/manual/en/function.implode.php + * Implode an array with the key and value pair giving + * a glue, a separator between pairs and the array + * to implode. + * @param string $glue The glue between key and value + * @param string $separator Separator between pairs + * @param array $array The array to implode + * @return string The imploded array + */ + function array_implode( $glue, $separator, $array ) { + + if ( !is_array( $array ) ) return $array; + + $string = array(); + foreach ( $array as $key => $val ) { + if ( is_array( $val ) ) + $val = implode( ',', $val ); + + $string[] = "{$key}{$glue}{$val}"; + } + + return implode( $separator, $string ); + } + + /* elapsed time */ + function elapsedTime($sec){ + + if(!$sec || !is_numeric($sec)) return ""; + + $days = floor($sec / 86400); + $hrs = floor(bcmod($sec,86400)/3600); + $mins = round(bcmod(bcmod($sec,86400),3600)/60); + if($days > 0) $tstring = $days . 'd,'; + if($hrs > 0) $tstring = $tstring . $hrs . 'h,'; + $tstring =$tstring . $mins . 'm'; + + return $tstring; + } + + /* Dates helpers...most of this crap will change once we move to PHP 5*/ + function db_date($time) { + global $cfg; + return Format::userdate($cfg->getDateFormat(),Misc::db2gmtime($time)); + } + + function db_datetime($time) { + global $cfg; + return Format::userdate($cfg->getDateTimeFormat(),Misc::db2gmtime($time)); + } + + function db_daydatetime($time) { + global $cfg; + return Format::userdate($cfg->getDayDateTimeFormat(),Misc::db2gmtime($time)); + } + + function userdate($format,$gmtime) { + return Format::date($format,$gmtime,$_SESSION['TZ_OFFSET'],$_SESSION['daylight']); + } + + function date($format,$gmtimestamp,$offset=0,$daylight=false){ + if(!$gmtimestamp || !is_numeric($gmtimestamp)) return ""; + + $offset+=$daylight?date('I',$gmtimestamp):0; //Daylight savings crap. + return date($format,($gmtimestamp+($offset*3600))); + } + + + + +} +?> diff --git a/include/class.group.php b/include/class.group.php new file mode 100644 index 0000000000000000000000000000000000000000..1b21dce29e4511b469c8dfb70386e7e940626d48 --- /dev/null +++ b/include/class.group.php @@ -0,0 +1,155 @@ +<?php +/********************************************************************* + class.group.php + + User Group - Everything about a group! + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Group { + + var $id; + var $ht; + + function Group($id){ + + $this->id=0; + return $this->load($id); + } + + function load($id){ + + $sql='SELECT grp.*,grp.group_name as name, grp.group_enabled as isactive, count(staff.staff_id) as users ' + .'FROM '.GROUP_TABLE.' grp ' + .'LEFT JOIN '.STAFF_TABLE.' staff USING(group_id) ' + .'WHERE grp.group_id='.db_input($id).' GROUP BY grp.group_id '; + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['group_id']; + $this->members=array(); + + return $this->id; + } + + function reload(){ + return $this->load($this->getId()); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo(){ + return $this->getHashtable(); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['name']; + } + + function getNumUsers(){ + return $this->ht['users']; + } + + + function isEnabled(){ + return ($this->ht['isactive']); + } + + function isActive(){ + return $this->isEnabled(); + } + + + + function update($vars,&$errors) { + + if(Group::save($this->getId(),$vars,$errors)){ + $this->reload(); + return true; + } + + return false; + } + + /*** Static functions ***/ + function getIdByName($name){ + $sql='SELECT group_id FROM '.GROUP_TABLE.' WHERE group_name='.db_input(trim($name)); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($g= new Group($id)) && $g->getId()==$id)?$g:null; + } + + + function create($vars,&$errors) { + return self::save(0,$vars,$errors); + } + + function save($id,$vars,&$errors) { + + if($id && $vars['id']!=$id) + $errors['err']='Missing or invalid group ID'; + + if(!$vars['name']) { + $errors['name']='Group name required'; + }elseif(strlen($vars['name'])<3) { + $errors['name']='Group name must be at least 3 chars.'; + }elseif(($gid=Group::getIdByName($vars['name'])) && $gid!=$id){ + $errors['name']='Group name already exists'; + } + + if($errors) return false; + + $sql=' SET updated=NOW(), group_name='.db_input(Format::striptags($vars['name'])). + ', group_enabled='.db_input($vars['isactive']). + ', dept_access='.db_input($vars['depts']?implode(',',$vars['depts']):''). + ', can_create_tickets='.db_input($vars['can_create_tickets']). + ', can_delete_tickets='.db_input($vars['can_delete_tickets']). + ', can_edit_tickets='.db_input($vars['can_edit_tickets']). + ', can_assign_tickets='.db_input($vars['can_assign_tickets']). + ', can_transfer_tickets='.db_input($vars['can_transfer_tickets']). + ', can_close_tickets='.db_input($vars['can_close_tickets']). + ', can_ban_emails='.db_input($vars['can_ban_emails']). + ', can_manage_premade='.db_input($vars['can_manage_premade']). + ', can_manage_faq='.db_input($vars['can_manage_faq']). + ', notes='.db_input($vars['notes']); + + if($id) { + + $sql='UPDATE '.GROUP_TABLE.' '.$sql.' WHERE group_id='.db_input($id); + if(($res=db_query($sql))) + return true; + + $errors['err']='Unable to update group. Internal error occurred.'; + + }else{ + $sql='INSERT INTO '.GROUP_TABLE.' '.$sql.',created=NOW()'; + if(($res=db_query($sql)) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create the group. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.http.php b/include/class.http.php new file mode 100644 index 0000000000000000000000000000000000000000..d134e6b8f3aef21f50440b175d8aa7249dc08676 --- /dev/null +++ b/include/class.http.php @@ -0,0 +1,54 @@ +<?php +/********************************************************************* + class.http.php + + Http helper. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Http { + + function header_code_verbose($code) { + switch($code): + case 200: return '200 OK'; + case 201: return '201 Created'; + case 204: return '204 NoContent'; + case 400: return '400 Bad Request'; + case 401: return '401 Unauthorized'; + case 403: return '403 Forbidden'; + case 404: return '404 Not Found'; + case 405: return '405 Method Not Allowed'; + case 416: return '416 Requested Range Not Satisfiable'; + default: return '500 Internal Server Error'; + endswitch; + } + + function response($code,$content,$contentType='text/html',$charset='UTF-8') { + + header('HTTP/1.1 '.Http::header_code_verbose($code)); + header('Status: '.Http::header_code_verbose($code)."\r\n"); + header("Connection: Close\r\n"); + header("Content-Type: $contentType; charset=$charset\r\n"); + header('Content-Length: '.strlen($content)."\r\n\r\n"); + print $content; + exit; + } + + function redirect($url,$delay=0,$msg='') { + + if(strstr($_SERVER['SERVER_SOFTWARE'], 'IIS')){ + header("Refresh: $delay; URL=$url"); + }else{ + header("Location: $url"); + } + exit; + } +} +?> diff --git a/include/class.json.php b/include/class.json.php new file mode 100644 index 0000000000000000000000000000000000000000..f983c73e42fe0394e5a133baebab5e6b26fa639f --- /dev/null +++ b/include/class.json.php @@ -0,0 +1,62 @@ +<?php +/********************************************************************* + class.json.php + + Parses JSON text data to PHP associative array. Useful mainly for API + JSON requests. The module will attempt to use the json_* functions + builtin to PHP5.2+ if they exist and will fall back to a pure-php + implementation included in JSON.php. + + Jared Hancock + Copyright (c) 2006-2010 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: + $Id: $ +**********************************************************************/ + +include_once "JSON.php"; + +class JsonDataParser { + function parse($stream) { + $contents = ''; + while (!feof($stream)) { + $contents .= fread($stream, 8192); + } + if (function_exists("json_decode")) { + return json_decode($contents, true); + } else { + # Create associative arrays rather than 'objects' + $decoder = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); + return $decoder->decode($contents); + } + } + function lastError() { + if (function_exists("json_last_error")) { + $errors = array( + JSON_ERROR_NONE => 'No errors', + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' + ); + if ($message = $errors[json_last_error()]) + return $message; + return "Unknown error"; + } else { + # Doesn't look like Servies_JSON supports errors for decode() + return "Unknown JSON parsing error"; + } + } +} + +class JsonDataEncoder { + function encode($var) { + $decoder = new Services_JSON(); + return $decoder->encode($var); + } +} diff --git a/include/class.knowledgebase.php b/include/class.knowledgebase.php new file mode 100644 index 0000000000000000000000000000000000000000..d8a64689168de48014011bf46c45e029056efdea --- /dev/null +++ b/include/class.knowledgebase.php @@ -0,0 +1,153 @@ +<?php +/********************************************************************* + class.knowledgebase.php + + Backend support for knowledgebase creates, edits, deletes, and + attachments. + + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once("class.file.php"); + +class Knowledgebase { + + function Knowledgebase($id) { + $res=db_query( + 'SELECT title, isenabled, dept_id, created, updated ' + .'FROM '.CANNED_TABLE.' WHERE canned_id='.db_input($id)); + if (!$res || !db_num_rows($res)) return false; + list( $this->title, + $this->enabled, + $this->department, + $this->created, + $this->updated) = db_fetch_row($res); + $this->id = $id; + $this->_attachments = new AttachmentList( + CANNED_ATTACHMENT_TABLE, 'canned_id='.db_input($id)); + } + + /* ------------------> Getter methods <--------------------- */ + function getTitle() { return $this->title; } + function isEnabled() { return !!$this->enabled; } + function getAnswer() { + if (!isset($this->answer)) { + if ($res=db_query('SELECT answer FROM '.CANNED_TABLE + .' WHERE canned_id='.db_input($this->id))) { + list($this->answer)=db_fetch_row($res); + } + } + return $this->answer; + } + function getCreated() { return $this->created; } + function lastUpdated() { return $this->updated; } + function attachments() { return $this->_attachments; } + function getDeptId() { return $this->department; } + function getDepartment() { return new Dept($this->department); } + function getId() { return $this->id; } + + /* ------------------> Setter methods <--------------------- */ + function publish() { $this->published = true; } + function unpublish() { $this->published = false; } + function setPublished($val) { $this->published = !!$val; } + function setTitle($title) { $this->title = $title; } + function setKeywords($words) { $this->keywords = $words; } + function setAnswer($text) { $this->answer = $text; } + + /* -------------> Validation and Clean methods <------------ */ + function validate(&$errors, $what=null) { + if (!$what) $what=$this->getHashtable(); + else $this->clean($what); + # TODO: Validate current values ($this->yada) + # Apply hashtable to this -- return error list + $validation = array( + 'title' => array('is_string', 'Title is required') + ); + foreach ($validation as $key=>$details) { + list($func, $error) = $details; + if (!call_user_func($func, $what[$key])) { + $errors[$key] = $error; + } + } + return count($errors) == 0; + } + + function clean(&$what) { + if (isset($what['topic'])) + $what['topic']=Format::striptags(trim($what['topic'])); + } + + function getHashtable() { + # TODO: Return hashtable like the one that would be passed into + # $this->save() or self::create() + return array('title'=>$this->title, 'department'=>$this->department, + 'isenabled'=>$this->enabled); + } + + /* -------------> Database access methods <----------------- */ + function update() { + if (!@$this->validate()) return false; + db_query( + 'UPDATE '.CANNED_TABLE.' SET title='.db_input($this->title) + .', isenabled='.db_input($this->enabled) + .', dept_id='.db_input($this->department) + .', updated=NOW()' + .((isset($this->answer)) + ? ', answer='.db_input($this->answer) : '') + .' WHERE canned_id='.db_input($this->id)); + return db_affected_rows() == 1; + } + function delete() { + db_query('DELETE FROM '.CANNED_TABLE.' WHERE canned_id=' + .db_input($this->id)); + return db_affected_rows() == 1; + } + /* For ->attach() and ->detach(), use $this->attachments() */ + function attach($file) { return $this->_attachments->add($file); } + function detach($file) { return $this->_attachments->remove($file); } + + /* ------------------> Static methods <--------------------- */ + function create($hash, &$errors) { + if (!self::validate($hash, $errors)) return false; + db_query('INSERT INTO '.CANNED_TABLE + .' (title, answer, department, isenabled, created, updated) VALUES (' + .db_input($hash['title']).',' + .db_input($hash['answer']).',' + .db_input($hash['dept']).',' + .db_input($hash['isenabled']).',NOW(),NOW()'); + return db_insert_id(); + } + + function save($id, $new_stuff, &$errors) { + if (!$id) return self::create($new_stuff, $errors); + if (!self::validate($errors, $new_stuff)) return false; + + # else + if (!($obj = new Knowledgebase($id))) { return false; } + $obj->setEnabled($new_stuff['enabled']); + $obj->setTitle($new_stuff['title']); + $obj->setAnswer($new_stuff['answer']); + $obj->setDepartment($new_stuff['dept']); + + return $obj->update(); + } + + function findByTitle($title) { + $res=db_query('SELECT canned_id FROM '.CANNED_TABLE + .' WHERE title LIKE '.db_input($title)); + if (list($id) = db_fetch_row($res)) { + return new Knowledgebase($id); + } + return false; + } + + function lookup($id) { + return ($id && is_numeric($id) && ($obj= new Knowledgebase($id)) && $obj->getId()==$id) + ? $obj : null; + } +} diff --git a/include/class.lock.php b/include/class.lock.php new file mode 100644 index 0000000000000000000000000000000000000000..caa1be71a99ba8b35a2406658aa974e11512cdea --- /dev/null +++ b/include/class.lock.php @@ -0,0 +1,157 @@ +<?php +/********************************************************************* + class.lock.php + + Ticket lock handle. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +/* + * Mainly used as a helper... + */ + +class TicketLock { + var $id; + var $ht; + + function TicketLock($id, $tid=0) { + $this->id=0; + $this->load($id, $tid); + } + + function load($id=0, $tid=0) { + + if(!$id && $this->ht['id']) + $id=$this->ht['id']; + + $sql='SELECT l.*, TIME_TO_SEC(TIMEDIFF(expire,NOW())) as timeleft ' + .' ,IF(s.staff_id IS NULL,"staff",CONCAT_WS(" ", s.lastname, s.firstname)) as staff ' + .' FROM '.TICKET_LOCK_TABLE. ' l ' + .' LEFT JOIN '.STAFF_TABLE.' s ON(s.staff_id=l.staff_id) ' + .' WHERE lock_id='.db_input($id); + + if($tid) + $sql.=' AND ticket_id='.db_input($tid); + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['id']=$this->ht['lock_id']; + $this->ht['expiretime']=time()+$this->ht['timeleft']; + + return true; + } + + function reload() { + return $this->load(); + } + + function getId() { + return $this->id; + } + + function getStaffId() { + return $this->ht['staff_id']; + } + + function getStaffName() { + return $this->ht['staff']; + } + + function getCreateTime() { + return $this->ht['created']; + } + + function getExpireTime() { + return $this->ht['expire']; + } + //Get remaiming time before the lock expires + function getTime() { + return $this->isExpired()?0:($this->ht['expiretime']-time()); + } + + //Should we be doing realtime check here? (Ans: not really....expiretime is local & based on loadtime) + function isExpired() { + return (time()>$this->ht['expiretime']); + } + + //Renew existing lock. + function renew($lockTime=0) { + + if(!$lockTime || !is_numeric($lockTime)) //XXX: test to make it works. + $lockTime = '(TIME_TO_SEC(TIMEDIFF(created,expire))/60)'; + + + $sql='UPDATE '.TICKET_LOCK_TABLE + .' SET expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) ' + .' WHERE lock_id='.db_input($this->getId()); + //echo $sql; + if(!db_query($sql) || !db_affected_rows()) + return false; + + $this->reload(); + + return true; + } + + //release aka delete a lock. + function release() { + //FORCED release - we don't give a .... + $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE lock_id='.db_input($this->getId()).' LIMIT 1'; + return (db_query($sql) && db_affected_rows()); + } + + /* ----------------------- Static functions ---------------------------*/ + function lookup($id, $tid) { + return ($id && ($lock = new TicketLock($id,$tid)) && $lock->getId()==$id)?$lock:null; + } + + //Create a ticket lock...this function assumes the caller checked for access & validity of ticket & staff x-ship. + function acquire($ticketId, $staffId, $lockTime) { + + if(!$ticketId or !$staffId or !$lockTime) + return 0; + + + //Cleanup any expired locks on the ticket. + db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE ticket_id='.db_input($ticketId).' AND expire<NOW()'); + //create the new lock. + $sql='INSERT IGNORE INTO '.TICKET_LOCK_TABLE.' SET created=NOW() ' + .',ticket_id='.db_input($ticketId) + .',staff_id='.db_input($staffId) + .',expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) '; + + return db_query($sql)?db_insert_id():0; + } + + function create($ticketId, $staffId, $lockTime) { + if(($id=self::acquire($ticketId, $staffId, $lockTime))) + return self::lookup($id); + } + + //Simply remove ALL locks a user (staff) holds on a ticket(s). + function removeStaffLocks($staffId, $ticketId=0) { + $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE staff_id='.db_input($staffId); + if($ticketId) + $sql.=' AND ticket_id='.db_input($ticketId); + + return db_query($sql); + } + + //Called via cron + function cleanup() { + //Cleanup any expired locks. + db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE expire<NOW()'); + @db_query('OPTIMIZE TABLE '.TICKET_LOCK_TABLE); + } +} +?> diff --git a/include/class.log.php b/include/class.log.php new file mode 100644 index 0000000000000000000000000000000000000000..9a2394cfff5cab638f5edef8bab331d88b7dbb18 --- /dev/null +++ b/include/class.log.php @@ -0,0 +1,76 @@ +<?php +/********************************************************************* + class.group.php + + User Group - Everything about a group! + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Log { + + var $id; + var $info; + + function Log($id){ + $this->id=0; + return $this->load($id); + } + + function load($id){ + + $sql='SELECT * FROM '.SYSLOG_TABLE.' WHERE log_id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->info=db_fetch_array($res); + $this->id=$this->info['log_id']; + + return $this->id; + } + + function reload(){ + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getType(){ + return $this->info['log_type']; + } + + function getTitle(){ + return $this->info['title']; + } + + function getText(){ + return $this->info['log']; + } + + function getIp(){ + return $this->info['ip_address']; + } + + function getCreateDate(){ + return $this->info['created']; + } + + function getInfo(){ + return $this->info; + } + + /*** static function ***/ + function lookup($id){ + return ($id && is_numeric($id) && ($l= new Log($id)) && $l->getId()==$id)?$l:null; + } +} +?> diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php new file mode 100644 index 0000000000000000000000000000000000000000..5f97775930f25da87f80261cc20b66cdad947b8f --- /dev/null +++ b/include/class.mailfetch.php @@ -0,0 +1,419 @@ +<?php +/********************************************************************* + class.mailfetch.php + + mail fetcher class. Uses IMAP ext for now. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once(INCLUDE_DIR.'class.mailparse.php'); +require_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.dept.php'); +require_once(INCLUDE_DIR.'class.filter.php'); + +class MailFetcher { + var $hostname; + var $username; + var $password; + + var $port; + var $protocol; + var $encryption; + + var $mbox; + + var $charset= 'UTF-8'; + + function MailFetcher($username,$password,$hostname,$port,$protocol,$encryption='') { + + if(!strcasecmp($protocol,'pop')) //force pop3 + $protocol='pop3'; + + $this->hostname=$hostname; + $this->username=$username; + $this->password=$password; + $this->protocol=strtolower($protocol); + $this->port = $port; + $this->encryption = $encryption; + + $this->serverstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); + if(!strcasecmp($this->encryption,'SSL')){ + $this->serverstr.='/ssl'; + } + $this->serverstr.='/novalidate-cert}INBOX'; //add other flags here as needed. + + //echo $this->serverstr; + //Charset to convert the mail to. + $this->charset='UTF-8'; + //Set timeouts + if(function_exists('imap_timeout')) + imap_timeout(1,20); //Open timeout. + } + + function connect() { + return $this->open()?true:false; + } + + function open() { + + //echo $this->serverstr; + if($this->mbox && imap_ping($this->mbox)) + return $this->mbox; + + $this->mbox =@imap_open($this->serverstr,$this->username,$this->password); + + return $this->mbox; + } + + function close() { + imap_close($this->mbox,CL_EXPUNGE); + } + + function mailcount(){ + return count(imap_headers($this->mbox)); + } + + //Get mail boxes. + function getMailboxes(){ + + + $mstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); + if(!strcasecmp($this->encryption,'SSL')) + $mstr.='/ssl'; + $mstr.='/novalidate-cert}'; + $list=array(); + if(($folders=imap_listmailbox($this->mbox,$mstr,'*')) && is_array($folders)){ + foreach($folders as $k=>$folder){ + $list[]= str_replace($mstr, "", imap_utf7_decode(trim($folder))); + } + } + + return $list; + } + + //Create a folder. + function createMailbox($folder){ + + if(!$folder) return false; + + $mstr=sprintf('{%s:%d/%s',$this->hostname,$this->port,strtolower($this->protocol)); + if(!strcasecmp($this->encryption,'SSL')) + $mstr.='/ssl'; + $mstr.='/novalidate-cert}'.$folder; + + return imap_createmailbox($this->mbox,imap_utf7_encode($mstr)); + } + + /* check if a folder exits - create on if requested */ + function checkMailbox($folder,$create=false){ + + if(($mailboxes=$this->getMailboxes()) && in_array($folder,$mailboxes)) + return true; + + return ($create && $this->createMailbox($folder)); + } + + + function decode($encoding,$text) { + + switch($encoding) { + case 1: + $text=imap_8bit($text); + break; + case 2: + $text=imap_binary($text); + break; + case 3: + $text=imap_base64($text); + break; + case 4: + $text=imap_qprint($text); + break; + case 5: + default: + $text=$text; + } + return $text; + } + + //Convert text to desired encoding..defaults to utf8 + function mime_encode($text,$charset=null,$enc='utf-8') { //Thank in part to afterburner + + $encodings=array('UTF-8','WINDOWS-1251', 'ISO-8859-5', 'ISO-8859-1','KOI8-R'); + if(function_exists("iconv") and $text) { + if($charset) + return iconv($charset,$enc.'//IGNORE',$text); + elseif(function_exists("mb_detect_encoding")) + return iconv(mb_detect_encoding($text,$encodings),$enc,$text); + } + + return utf8_encode($text); + } + + //Generic decoder - mirrors imap_utf8 + function mime_decode($text) { + + $a = imap_mime_header_decode($text); + $str = ''; + foreach ($a as $k => $part) + $str.= $part->text; + + return $str?$str:imap_utf8($text); + } + + function getLastError(){ + return imap_last_error(); + } + + function getMimeType($struct) { + $mimeType = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER'); + if(!$struct || !$struct->subtype) + return 'TEXT/PLAIN'; + + return $mimeType[(int) $struct->type].'/'.$struct->subtype; + } + + function getHeaderInfo($mid) { + + $headerinfo=imap_headerinfo($this->mbox,$mid); + $sender=$headerinfo->from[0]; + + //Parse what we need... + $header=array( + 'from' =>array('name' =>@$sender->personal,'email' =>strtolower($sender->mailbox).'@'.$sender->host), + 'subject'=>@$headerinfo->subject, + 'mid' =>$headerinfo->message_id); + return $header; + } + + //search for specific mime type parts....encoding is the desired encoding. + function getPart($mid,$mimeType,$encoding=false,$struct=null,$partNumber=false){ + + if(!$struct && $mid) + $struct=@imap_fetchstructure($this->mbox, $mid); + //Match the mime type. + if($struct && !$struct->ifdparameters && strcasecmp($mimeType,$this->getMimeType($struct))==0){ + $partNumber=$partNumber?$partNumber:1; + if(($text=imap_fetchbody($this->mbox, $mid, $partNumber))){ + if($struct->encoding==3 or $struct->encoding==4) //base64 and qp decode. + $text=$this->decode($struct->encoding,$text); + + $charset=null; + if($encoding) { //Convert text to desired mime encoding... + if($struct->ifparameters){ + if(!strcasecmp($struct->parameters[0]->attribute,'CHARSET') && strcasecmp($struct->parameters[0]->value,'US-ASCII')) + $charset=trim($struct->parameters[0]->value); + } + $text=$this->mime_encode($text,$charset,$encoding); + } + return $text; + } + } + //Do recursive search + $text=''; + if($struct && $struct->parts){ + while(list($i, $substruct) = each($struct->parts)) { + if($partNumber) + $prefix = $partNumber . '.'; + if(($result=$this->getPart($mid,$mimeType,$encoding,$substruct,$prefix.($i+1)))) + $text.=$result; + } + } + return $text; + } + + function getHeader($mid){ + return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT); + } + + + function getPriority($mid){ + return Mail_Parse::parsePriority($this->getHeader($mid)); + } + + function getBody($mid) { + + $body =''; + if(!($body = $this->getpart($mid,'TEXT/PLAIN',$this->charset))) { + if(($body = $this->getPart($mid,'TEXT/HTML',$this->charset))) { + //Convert tags of interest before we striptags + $body=str_replace("</DIV><DIV>", "\n", $body); + $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); + $body=Format::html($body); //Balance html tags before stripping. + $body=Format::striptags($body); //Strip tags?? + } + } + return $body; + } + + function createTicket($mid,$emailid=0){ + global $cfg; + + $mailinfo=$this->getHeaderInfo($mid); + + //Make sure the email is NOT one of the undeleted emails. + if($mailinfo['mid'] && ($id=Ticket::getIdByMessageId(trim($mailinfo['mid']),$mailinfo['from']['email']))){ + //TODO: Move emails to a fetched folder when delete is false?? + return true; + } + + //Is the email address banned? + if($mailinfo['from']['email'] && EmailFilter::isBanned($mailinfo['from']['email'])) { + //We need to let admin know... + Sys::log(LOG_WARNING,'Ticket denied','Banned email - '.$mailinfo['from']['email']); + return true; + } + + + $var['name']=$this->mime_decode($mailinfo['from']['name']); + $var['email']=$mailinfo['from']['email']; + $var['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; + $var['message']=Format::stripEmptyLines($this->getBody($mid)); + $var['header']=$this->getHeader($mid); + $var['emailId']=$emailid?$emailid:$cfg->getDefaultEmailId(); //ok to default? + $var['name']=$var['name']?$var['name']:$var['email']; //No name? use email + $var['mid']=$mailinfo['mid']; + + if($cfg->useEmailPriority()) + $var['pri']=$this->getPriority($mid); + + $ticket=null; + $newticket=true; + //Check the subject line for possible ID. + if(preg_match ("[[#][0-9]{1,10}]",$var['subject'],$regs)) { + $extid=trim(preg_replace("/[^0-9]/", "", $regs[0])); + $ticket= new Ticket(Ticket::getIdByExtId($extid)); + //Allow mismatched emails?? For now NO. + if(!$ticket || strcasecmp($ticket->getEmail(),$var['email'])) + $ticket=null; + } + + $errors=array(); + if(!$ticket) { + # Apply email filters for the new ticket + $ef = new EmailFilter($var); $ef->apply($var); + if(!($ticket=Ticket::create($var,$errors,'Email')) || $errors) + return null; + $msgid=$ticket->getLastMsgId(); + }else{ + $message=$var['message']; + //Strip quoted reply...TODO: figure out how mail clients do it without special tag.. + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()) && strpos($var['message'],$tag)) + list($message)=split($tag,$var['message']); + $msgid=$ticket->postMessage($message,'Email',$var['mid'],$var['header']); + } + //Save attachments if any. + if($msgid && $cfg->allowEmailAttachments()){ + if(($struct = imap_fetchstructure($this->mbox,$mid)) && $struct->parts) { + if($ticket->getLastMsgId()!=$msgid) + $ticket->setLastMsgId($msgid); + $this->saveAttachments($ticket,$mid,$struct); + + } + } + return $ticket; + } + + function saveAttachments($ticket,$mid,$part,$index=0) { + global $cfg; + + if($part && $part->ifdparameters && ($filename=$part->dparameters[0]->value)){ //attachment + $index=$index?$index:1; + if($ticket && $cfg->canUploadFileType($filename) && $cfg->getMaxFileSize()>=$part->bytes) { + //extract the attachments...and do the magic. + $data=$this->decode($part->encoding, imap_fetchbody($this->mbox,$mid,$index)); + $ticket->saveAttachment($filename,$data,$ticket->getLastMsgId(),'M'); + return; + } + //TODO: Log failure?? + } + + //Recursive attachment search! + if($part && $part->parts) { + foreach($part->parts as $k=>$struct) { + if($index) $prefix = $index.'.'; + $this->saveAttachments($ticket,$mid,$struct,$prefix.($k+1)); + } + } + + } + + function fetchTickets($emailid,$max=20,$deletemsgs=false){ + + $nummsgs=imap_num_msg($this->mbox); + //echo "New Emails: $nummsgs\n"; + $msgs=$errors=0; + for($i=$nummsgs; $i>0; $i--){ //process messages in reverse. Latest first. FILO. + if($this->createTicket($i,$emailid)){ + imap_setflag_full($this->mbox, imap_uid($this->mbox,$i), "\\Seen", ST_UID); //IMAP only?? + if($deletemsgs) + imap_delete($this->mbox,$i); + $msgs++; + $errors=0; //We are only interested in consecutive errors. + }else{ + $errors++; + } + if(($max && $msgs>=$max) || $errors>20) + break; + } + @imap_expunge($this->mbox); + + return $msgs; + } + + function fetchMail(){ + global $cfg; + + if(!$cfg->canFetchMail()) + return; + + //We require imap ext to fetch emails via IMAP/POP3 + if(!function_exists('imap_open')) { + $msg='PHP must be compiled with IMAP extension enabled for IMAP/POP3 fetch to work!'; + Sys::log(LOG_WARN,'Mail Fetch Error',$msg); + return; + } + + $MAX_ERRORS=5; //Max errors before we start delayed fetch attempts - hardcoded for now. + + $sql=' SELECT email_id,mail_host,mail_port,mail_protocol,mail_encryption,mail_delete,mail_errors,userid,userpass FROM '.EMAIL_TABLE. + ' WHERE mail_active=1 AND (mail_errors<='.$MAX_ERRORS.' OR (TIME_TO_SEC(TIMEDIFF(NOW(),mail_lasterror))>5*60) )'. + ' AND (mail_lastfetch IS NULL OR TIME_TO_SEC(TIMEDIFF(NOW(),mail_lastfetch))>mail_fetchfreq*60) '; + //echo $sql; + if(!($accounts=db_query($sql)) || !db_num_rows($accounts)) + return; + + //TODO: Lock the table here?? + while($row=db_fetch_array($accounts)) { + $fetcher = new MailFetcher($row['userid'],Misc::decrypt($row['userpass'],SECRET_SALT), + $row['mail_host'],$row['mail_port'],$row['mail_protocol'],$row['mail_encryption']); + if($fetcher->connect()){ + $fetcher->fetchTickets($row['email_id'],$row['mail_fetchmax'],$row['mail_delete']?true:false); + $fetcher->close(); + db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=0, mail_lastfetch=NOW() WHERE email_id='.db_input($row['email_id'])); + }else{ + $errors=$row['mail_errors']+1; + db_query('UPDATE '.EMAIL_TABLE.' SET mail_errors=mail_errors+1, mail_lasterror=NOW() WHERE email_id='.db_input($row['email_id'])); + if($errors>=$MAX_ERRORS){ + //We've reached the MAX consecutive errors...will attempt logins at delayed intervals + $msg="\nThe system is having trouble fetching emails from the following mail account: \n". + "\nUser: ".$row['userid']. + "\nHost: ".$row['mail_host']. + "\nError: ".$fetcher->getLastError(). + "\n\n ".$errors.' consecutive errors. Maximum of '.$MAX_ERRORS. ' allowed'. + "\n\n This could be connection issues related to the host. Next delayed login attempt in aprox. 10 minutes"; + Sys::alertAdmin('Mail Fetch Failure Alert',$msg,true); + } + } + } + } +} +?> diff --git a/include/class.mailparse.php b/include/class.mailparse.php new file mode 100644 index 0000000000000000000000000000000000000000..88648367615c7141e2c87b4534f3df683d4ef081 --- /dev/null +++ b/include/class.mailparse.php @@ -0,0 +1,217 @@ +<?php +/********************************************************************* + class.mailparse.php + + Mail parsing helper class. + Mail parsing will change once we move to PHP5 + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once('Mail/mimeDecode.php'); +require_once('Mail/RFC822.php'); + +class Mail_Parse { + + var $mime_message; + var $include_bodies; + var $decode_headers; + var $decode_bodies; + + var $struct; + + function Mail_parse($mimeMessage,$includeBodies=true,$decodeHeaders=TRUE,$decodeBodies=TRUE){ + + $this->mime_message=$mimeMessage; + $this->include_bodies=$includeBodies; + $this->decode_headers=$decodeHeaders; + $this->decode_bodies=$decodeBodies; + } + + function decode() { + + $params = array('crlf' => "\r\n", + 'input' =>$this->mime_message, + 'include_bodies'=> $this->include_bodies, + 'decode_headers'=> $this->decode_headers, + 'decode_bodies' => $this->decode_bodies); + $this->splitBodyHeader(); + $this->struct=Mail_mimeDecode::decode($params); + + return (PEAR::isError($this->struct) || !(count($this->struct->headers)>1))?FALSE:TRUE; + } + + function splitBodyHeader() { + + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s",$this->mime_message, $match)) { + $this->header=$match[1]; + } + } + /** + * Takes the header section of an email message with the form of + * Header: Value + * and returns a hashtable of header-name => value pairs. Also, this + * function properly handles header values that span multiple lines + * (such as Content-Type). + * + * Specify $as_array to TRUE to keep all header values. If a header is + * specified more than once, all the values are placed in an array under + * the header key. If left as FALSE, only the value given in the last + * occurance of the header is retained. + */ + /* static */ function splitHeaders($headers_text, $as_array=false) { + $headers = preg_split("/\r?\n/", $headers_text); + for ($i=0, $k=count($headers); $i<$k; $i++) { + # XXX: Might tabs be used here? + if (substr($headers[$i], 0, 1) == " ") { + # Continuation from previous header (runon to next line) + $j=$i-1; while (!isset($headers[$j]) && $j>0) $j--; + $headers[$j] .= "\n".ltrim($headers[$i]); + unset($headers[$i]); + } elseif (strlen($headers[$i]) == 0) { + unset($headers[$i]); + } + } + $array = array(); + foreach ($headers as $hdr) { + list($name, $val) = explode(": ", $hdr, 2); + # Create list of values if header is specified more than once + if ($array[$name] && $as_array) { + if (is_array($array[$name])) $array[$name][] = $val; + else $array[$name] = array($array[$name], $val); + } else { + $array[$name] = $val; + } + } + return $array; + } + + + function getStruct(){ + return $this->struct; + } + + function getHeader() { + if(!$this->header) $this->splitBodyHeader(); + + return $this->header; + } + + function getError(){ + return PEAR::isError($this->struct)?$this->struct->getMessage():''; + } + + + function getFromAddressList(){ + return Mail_Parse::parseAddressList($this->struct->headers['from']); + } + + function getToAddressList(){ + //Delivered-to incase it was a BBC mail. + return Mail_Parse::parseAddressList($this->struct->headers['to']?$this->struct->headers['to']:$this->struct->headers['delivered-to']); + } + + function getCcAddressList(){ + return $this->struct->headers['cc']?Mail_Parse::parseAddressList($this->struct->headers['cc']):null; + } + + function getMessageId(){ + return $this->struct->headers['message-id']; + } + + function getSubject(){ + return $this->struct->headers['subject']; + } + + function getBody(){ + + $body=''; + if(!($body=$this->getPart($this->struct,'text/plain'))) { + if(($body=$this->getPart($this->struct,'text/html'))) { + //Cleanup the html. + $body=str_replace("</DIV><DIV>", "\n", $body); + $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); + $body=Format::striptags($body); + } + } + return $body; + } + + function getPart($struct,$ctypepart) { + + if($struct && !$struct->parts) { + $ctype = @strtolower($struct->ctype_primary.'/'.$struct->ctype_secondary); + if($ctype && strcasecmp($ctype,$ctypepart)==0) + return $struct->body; + } + + $data=''; + if($struct && $struct->parts) { + foreach($struct->parts as $i=>$part) { + if($part && !$part->disposition && ($text=$this->getPart($part,$ctypepart))) + $data.=$text; + } + } + return $data; + } + + function getAttachments($part=null){ + + if($part==null) + $part=$this->getStruct(); + + if($part && $part->disposition + && (!strcasecmp($part->disposition,'attachment') + || !strcasecmp($part->disposition,'inline') + || !strcasecmp($part->ctype_primary,'image'))){ + if(!($filename=$part->d_parameters['filename']) && $part->d_parameters['filename*']) + $filename=$part->d_parameters['filename*']; //Do we need to decode? + + return array(array('filename'=>$filename,'body'=>$part->body)); + } + + $files=array(); + if($part->parts){ + foreach($part->parts as $k=>$p){ + if($p && ($result=$this->getAttachments($p))) { + $files=array_merge($files,$result); + } + } + } + + return $files; + } + + function getPriority(){ + return Mail_Parse::parsePriority($this->getHeader()); + } + + function parsePriority($header=null){ + + $priority=0; + if($header && ($begin=strpos($header,'X-Priority:'))!==false){ + $begin+=strlen('X-Priority:'); + $xpriority=preg_replace("/[^0-9]/", "",substr($header, $begin, strpos($header,"\n",$begin) - $begin)); + if(!is_numeric($xpriority)) + $priority=0; + elseif($xpriority>4) + $priority=1; + elseif($xpriority>=3) + $priority=2; + elseif($xpriority>0) + $priority=3; + } + return $priority; + } + + function parseAddressList($address){ + return Mail_RFC822::parseAddressList($address, null, null,false); + } +} diff --git a/include/class.mcrypt.php b/include/class.mcrypt.php new file mode 100644 index 0000000000000000000000000000000000000000..a6a9791019d92ea2165b92c4ccd7e7e908380b6e --- /dev/null +++ b/include/class.mcrypt.php @@ -0,0 +1,43 @@ +<?php +/********************************************************************* + class.mcrypt.php + + Mcrypt wrapper.... nothing special at all. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Mcrypt { + + function encrypt($text, $salt){ + + //if mcrypt extension is not installed--simply return unencryted text and log a warning. + if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt')){ + $msg='Cryptography extension mcrypt is not enabled or installed. Important text/data is being stored as plain text in database.'; + Sys::log(LOG_WARN,'mcrypt module missing',$msg); + return $text; + } + + return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$salt, $text, MCRYPT_MODE_ECB, + mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)))); + } + + function decrypt($text, $salt){ + if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt')) + return $text; + + return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $salt, base64_decode($text), MCRYPT_MODE_ECB, + mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))); + } + + function exists(){ + return (function_exists('mcrypt_encrypt') && function_exists('mcrypt_decrypt')); + } +} +?> diff --git a/include/class.misc.php b/include/class.misc.php new file mode 100644 index 0000000000000000000000000000000000000000..c9a5db034865db1ac621948aacd3a03b9a919b1b --- /dev/null +++ b/include/class.misc.php @@ -0,0 +1,144 @@ +<?php +/********************************************************************* + class.misc.php + + Misc collection of useful generic helper functions. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Misc { + + function randCode($len=8) { + return substr(strtoupper(base_convert(microtime(),10,16)),0,$len); + } + + /* Helper used to generate ticket IDs */ + function randNumber($len=6,$start=false,$end=false) { + + mt_srand ((double) microtime() * 1000000); + $start=(!$len && $start)?$start:str_pad(1,$len,"0",STR_PAD_RIGHT); + $end=(!$len && $end)?$end:str_pad(9,$len,"9",STR_PAD_RIGHT); + + return mt_rand($start,$end); + } + + function encrypt($text, $salt) { + + //if mcrypt extension is not installed--simply return unencryted text and log a warning. + if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt')) { + $msg='Cryptography extension mcrypt is not enabled or installed. IMAP/POP passwords are being stored as plain text in database.'; + Sys::log(LOG_WARN,'mcrypt missing',$msg); + return $text; + } + + return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$salt, $text, MCRYPT_MODE_ECB, + mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)))); + } + + function decrypt($text, $salt) { + if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt')) + return $text; + + return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $salt, base64_decode($text), MCRYPT_MODE_ECB, + mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))); + } + + /* misc date helpers...this will go away once we move to php 5 */ + function db2gmtime($var){ + global $cfg; + if(!$var) return; + + $dbtime=is_int($var)?$var:strtotime($var); + return $dbtime-($cfg->getMysqlTZoffset()*3600); + } + + //Take user time or gmtime and return db (mysql) time. + function dbtime($var=null){ + global $cfg; + + if(is_null($var) || !$var) + $time=Misc::gmtime(); //gm time. + else{ //user time to GM. + $time=is_int($var)?$var:strtotime($var); + $offset=$_SESSION['TZ_OFFSET']+($_SESSION['daylight']?date('I',$time):0); + $time=$time-($offset*3600); + } + //gm to db time + return $time+($cfg->getMysqlTZoffset()*3600); + } + + /*Helper get GM time based on timezone offset*/ + function gmtime() { + return time()-date('Z'); + } + + //Current page + function currentURL() { + + $str = 'http'; + if ($_SERVER['HTTPS'] == 'on') { + $str .='s'; + } + $str .= '://'; + if (!isset($_SERVER['REQUEST_URI'])) { //IIS??? + $_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'],1 ); + if (isset($_SERVER['QUERY_STRING'])) { + $_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING']; + } + } + if ($_SERVER['SERVER_PORT']!=80) { + $str .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI']; + } else { + $str .= $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; + } + + return $str; + } + + function timeDropdown($hr=null, $min =null,$name='time') { + $hr =is_null($hr)?0:$hr; + $min =is_null($min)?0:$min; + + //normalize; + if($hr>=24) + $hr=$hr%24; + elseif($hr<0) + $hr=0; + + if($min>=45) + $min=45; + elseif($min>=30) + $min=30; + elseif($min>=15) + $min=15; + else + $min=0; + + ob_start(); + echo sprintf('<select name="%s" id="%s">',$name,$name); + echo '<option value="" selected>Time</option>'; + for($i=23; $i>=0; $i--) { + for($minute=45; $minute>=0; $minute-=15) { + $sel=($hr==$i && $min==$minute)?'selected="selected"':''; + $_minute=str_pad($minute, 2, '0',STR_PAD_LEFT); + $_hour=str_pad($i, 2, '0',STR_PAD_LEFT); + echo sprintf('<option value="%s:%s" %s>%s:%s</option>',$_hour,$_minute,$sel,$_hour,$_minute); + } + } + echo '</select>'; + $output = ob_get_contents(); + ob_end_clean(); + + return $output; + } + + +} +?> diff --git a/include/class.nav.php b/include/class.nav.php new file mode 100644 index 0000000000000000000000000000000000000000..561fc963dcd80165bc755fd96588907a3e06a1ac --- /dev/null +++ b/include/class.nav.php @@ -0,0 +1,287 @@ +<?php +/********************************************************************* + class.nav.php + + Navigation helper classes. Pointless BUT helps keep navigation clean and free from errors. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class StaffNav { + var $tabs=array(); + var $submenus=array(); + + var $activetab; + var $activemenu; + var $panel; + + var $staff; + + function StaffNav($staff, $panel='staff'){ + $this->staff=$staff; + $this->panel=strtolower($panel); + $this->tabs=$this->getTabs(); + $this->submenus=$this->getSubMenus(); + } + + function getPanel(){ + return $this->panel; + } + + function isAdminPanel(){ + return (!strcasecmp($this->getPanel(),'admin')); + } + + function isStaffPanel() { + return (!$this->isAdminPanel()); + } + + function setTabActive($tab){ + + if($this->tabs[$tab]){ + $this->tabs[$tab]['active']=true; + if($this->activetab && $this->activetab!=$tab && $this->tabs[$this->activetab]) + $this->tabs[$this->activetab]['active']=false; + + $this->activetab=$tab; + + return true; + } + + return false; + } + + function setActiveTab($tab){ + return $this->setTabActive($tab); + } + + function getActiveTab(){ + return $this->activetab; + } + + function setActiveSubMenu($mid) { + $this->activeMenu = $mid; + } + + function getActiveMenu() { + return $this->activeMenu; + } + + function addSubMenu($item,$active=false){ + + $this->submenus[$this->getPanel().'.'.$this->activetab][]=$item; + if($active) + $this->activeMenu=sizeof($this->submenus[$this->getPanel().'.'.$this->activetab]); + } + + + function getTabs(){ + + if(!$this->tabs) { + $this->tabs=array(); + $this->tabs['dashboard']=array('desc'=>'Dashboard','href'=>'dashboard.php','title'=>'Staff Dashboard'); + $this->tabs['tickets']=array('desc'=>'Tickets','href'=>'tickets.php','title'=>'Ticket Queue'); + $this->tabs['kbase']=array('desc'=>'Knowledgebase','href'=>'kb.php','title'=>'Knowledgebase'); + } + + return $this->tabs; + } + + function getSubMenus(){ //Private. + + $staff = $this->staff; + $submenus=array(); + foreach($this->getTabs() as $k=>$tab){ + $subnav=array(); + switch(strtolower($k)){ + case 'tickets': + $subnav[]=array('desc'=>'Tickets','href'=>'tickets.php','iconclass'=>'Ticket', 'droponly'=>true); + if($staff) { + if(($assigned=$staff->getNumAssignedTickets())) + $subnav[]=array('desc'=>"My Tickets ($assigned)", + 'href'=>'tickets.php?status=assigned', + 'iconclass'=>'assignedTickets', + 'droponly'=>true); + + if($staff->canCreateTickets()) + $subnav[]=array('desc'=>'New Ticket', + 'href'=>'tickets.php?a=open', + 'iconclass'=>'newTicket', + 'droponly'=>true); + } + break; + case 'dashboard': + $subnav[]=array('desc'=>'Dashboard','href'=>'dashboard.php','iconclass'=>'logs'); + $subnav[]=array('desc'=>'Staff Directory','href'=>'directory.php','iconclass'=>'teams'); + $subnav[]=array('desc'=>'My Profile','href'=>'profile.php','iconclass'=>'users'); + break; + case 'kbase': + $subnav[]=array('desc'=>'Knowledgebase','href'=>'kb.php', 'urls'=>array('faq.php'), 'iconclass'=>'premade'); + if($staff) { + if($staff->canManageFAQ()) + $subnav[]=array('desc'=>'Categories','href'=>'categories.php','iconclass'=>'premade'); + if($staff->canManageCannedResponses()) + $subnav[]=array('desc'=>'Canned Replies','href'=>'canned.php','iconclass'=>'premade'); + } + break; + } + if($subnav) + $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav; + } + + return $submenus; + } + + function getSubMenu($tab=null){ + $tab=$tab?$tab:$this->activetab; + return $this->submenus[$this->getPanel().'.'.$tab]; + } + + function getSubNav($tab=null){ + return $this->getSubMenu($tab); + } + +} + +class AdminNav extends StaffNav{ + + function AdminNav($staff){ + parent::StaffNav($staff, 'admin'); + } + + function getTabs(){ + + + if(!$this->tabs){ + + $tabs=array(); + $tabs['dashboard']=array('desc'=>'Dashboard','href'=>'admin.php','title'=>'Admin Dashboard'); + $tabs['settings']=array('desc'=>'Settings','href'=>'settings.php','title'=>'System Settings'); + $tabs['emails']=array('desc'=>'Emails','href'=>'emails.php','title'=>'Email Settings'); + $tabs['topics']=array('desc'=>'Help Topics','href'=>'helptopics.php','title'=>'Help Topics'); + $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Staff Members'); + $tabs['depts']=array('desc'=>'Departments','href'=>'departments.php','title'=>'Departments'); + $this->tabs=$tabs; + } + + return $this->tabs; + } + + function getSubMenus(){ + + $submenus=array(); + foreach($this->getTabs() as $k=>$tab){ + $subnav=array(); + switch(strtolower($k)){ + case 'dashboard': + $subnav[]=array('desc'=>'System Logs','href'=>'syslogs.php','iconclass'=>'logs'); + break; + case 'settings': + $subnav[]=array('desc'=>'Settings & Preferences','href'=>'settings.php','iconclass'=>'preferences'); + $subnav[]=array('desc'=>'SLA Plans','href'=>'slas.php','iconclass'=>'sla'); + $subnav[]=array('desc'=>'API Keys','href'=>'apikeys.php','iconclass'=>'api'); + break; + case 'emails': + $subnav[]=array('desc'=>'Email Addresses','href'=>'emails.php','iconclass'=>'emailSettings'); + $subnav[]=array('desc'=>'Email Filters','href'=>'filters.php', + 'title'=>'Email Filters','iconclass'=>'emailFilters'); + $subnav[]=array('desc'=>'Email Banlist','href'=>'banlist.php', + 'title'=>'Banned Emails','iconclass'=>'emailDiagnostic'); + $subnav[]=array('desc'=>'Email Templates','href'=>'templates.php','title'=>'Email Templates','iconclass'=>'emailTemplates'); + $subnav[]=array('desc'=>'Email Diagnostic','href'=>'emailtest.php','iconclass'=>'emailDiagnostic'); + break; + case 'topics': + $subnav[]=array('desc'=>'Help Topics','href'=>'helptopics.php','iconclass'=>'helpTopics'); + $subnav[]=array('desc'=>'Add New Help Topics', + 'href'=>'helptopics.php?a=add', + 'iconclass'=>'newHelpTopic', + 'droponly'=>true); + break; + case 'staff': + $subnav[]=array('desc'=>'Staff Members','href'=>'staff.php','iconclass'=>'users'); + $subnav[]=array('desc'=>'Teams','href'=>'teams.php','iconclass'=>'teams'); + $subnav[]=array('desc'=>'Groups','href'=>'groups.php','iconclass'=>'groups'); + break; + case 'depts': + $subnav[]=array('desc'=>'Departments','href'=>'departments.php','iconclass'=>'departments'); + $subnav[]=array('desc'=>'Add New Department', + 'href'=>'departments.php?a=add', + 'iconclass'=>'newDepartment', + 'droponly'=>true); + break; + } + if($subnav) + $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav; + } + + return $submenus; + } +} + +class UserNav { + + var $navs=array(); + var $activenav; + + var $user; + + function UserNav($user=null, $active=''){ + + $this->user=$user; + $this->navs=$this->getNavs(); + if($active) + $this->setActiveNav($active); + } + + function setActiveNav($nav){ + + if($nav && $this->navs[$nav]){ + $this->navs[$nav]['active']=true; + if($this->activenav && $this->activenav!=$nav && $this->navs[$this->activenav]) + $this->navs[$this->activenav]['active']=false; + + $this->activenav=$nav; + + return true; + } + + return false; + } + + function getNavLinks(){ + global $cfg; + + //Paths are based on the root dir. + if(!$this->navs){ + + $navs = array(); + $user = $this->user; + $navs['home']=array('desc'=>'Support Center Home','href'=>'index.php','title'=>''); + if($cfg && $cfg->isKnowledgebaseEnabled()) + $navs['kb']=array('desc'=>'Knowledgebase','href'=>'kb/index.php','title'=>''); + + $navs['new']=array('desc'=>'Open New Ticket','href'=>'open.php','title'=>''); + if($user && $user->isValid()) + $navs['tickets']=array('desc'=>'My Tickets','href'=>'tickets.php','title'=>''); + else + $navs['status']=array('desc'=>'Check Ticket Status','href'=>'view.php','title'=>''); + $this->navs=$navs; + } + + return $this->navs; + } + + function getNavs(){ + return $this->getNavLinks(); + } + +} + +?> diff --git a/include/class.ostsession.php b/include/class.ostsession.php new file mode 100644 index 0000000000000000000000000000000000000000..ccbae6e03f90147f06b24a79b0e1468faae4f821 --- /dev/null +++ b/include/class.ostsession.php @@ -0,0 +1,113 @@ +<?php +/********************************************************************* + class.ostsession.php + + Custom osTicket session handler. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class osTicketSession { + + var $ttl = SESSION_TTL; + + function osTicketSession($ttl=0){ + + $this->ttl =$ttl?$ttl:get_cfg_var('session.gc_maxlifetime'); + if(!$this->ttl) + $this->ttl=SESSION_TTL; + + //Set handlers. + session_set_save_handler( + array(&$this, 'open'), + array(&$this, 'close'), + array(&$this, 'read'), + array(&$this, 'write'), + array(&$this, 'destroy'), + array(&$this, 'gc') + ); + //Forced cleanup. + register_shutdown_function('session_write_close'); + //Start the session. + session_start(); + } + + function regenerate_id(){ + $oldId = session_id(); + session_regenerate_id(); + $this->destroy($oldId); + } + + function open($save_path, $session_name){ + return (true); + } + + function close(){ + return (true); + } + + function read($session_id){ + $data=""; + $sql='SELECT session_data FROM '.SESSION_TABLE.' WHERE session_id='.db_input($session_id).' AND session_expire>NOW()'; + if(($res=db_query($sql)) && db_num_rows($res)) + list($data)=db_fetch_row($res); + + return $data; + } + + function write($id, $data){ + global $cfg,$thisstaff; + + $sql='REPLACE INTO '.SESSION_TABLE.' SET session_updated=NOW() '. + ',session_id='.db_input($id). + ',session_data='.db_input($data). + ',session_expire=(NOW() + INTERVAL '.$this->getTTL().' SECOND)'. + ',user_id='.db_input($thisstaff?$thisstaff->getId():0). + ',user_ip='.db_input($_SERVER['REMOTE_ADDR']). + ',user_agent='.db_input($_SERVER['HTTP_USER_AGENT']); + + return (db_query($sql) && db_affected_rows()); + } + + function destroy($id){ + $sql='DELETE FROM '.SESSION_TABLE.' WHERE session_id='.db_input($id); + return (db_query($sql) && db_affected_rows()); + } + + function gc($maxlife){ + $sql='DELETE FROM '.SESSION_TABLE.' WHERE session_expire<NOW()'; + db_query($sql); + } + + /* helper functions */ + + function getTTL(){ + return $this->ttl; + } + + function get_online_users($sec=0){ + $sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()'; + if($sec) + $sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec"; + + $users=array(); + if(($res=db_query($sql)) && db_num_rows($res)){ + list($users[])=db_fetch_row($res); + } + + return $users; + } + + /* ---------- static function ---------- */ + function start($ttl=0) { + return New osTicketSession($ttl); + } +} +?> diff --git a/include/class.pagenate.php b/include/class.pagenate.php new file mode 100644 index 0000000000000000000000000000000000000000..b182db517e579a9a7f4921a086e57825cea026a5 --- /dev/null +++ b/include/class.pagenate.php @@ -0,0 +1,129 @@ +<?php +/********************************************************************* + class.format.php + + Pagenation support class + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class PageNate { + + var $start; + var $limit; + var $total; + var $page; + var $pages; + + + function PageNate($total,$page,$limit=20,$url='') { + $this->total = intval($total); + $this->limit = max($limit, 1 ); + $this->page = max($page, 1 ); + $this->start = max((($page-1)*$this->limit),0); + $this->pages = ceil( $this->total / $this->limit ); + + if (($this->limit > $this->total) || ($this->page>ceil($this->total/$this->limit))) { + $this->start = 0; + } + if (($this->limit-1)*$this->start > $this->total) { + $this->start -= $this->start % $this->limit; + } + $this->setURL($url); + } + function setURL($url='',$vars=''){ + if($url){ + if(strpos($url,'?')===false) + $url=$url.'?'; + }else{ + $url=THISPAGE.'?'; + } + $this->url=$url.$vars; + } + + function getStart() { + return $this->start; + } + + function getLimit() { + return $this->limit; + } + + + function getNumPages(){ + return $this->pages; + } + + function getPage() { + return ceil(($this->start+1)/$this->limit); + } + + function showing() { + $html = ''; + $from= $this->start+1; + if ($this->start + $this->limit < $this->total) { + $to= $this->start + $this->limit; + } else { + $to= $this->total; + } + $html=" Showing "; + if ($this->total > 0) { + $html .= "$from - $to of " .$this->total; + }else{ + $html .= " 0 "; + } + return $html; + } + + function getPageLinks() { + $html = ''; + $file =$this->url; + $displayed_span = 5; + $total_pages = ceil( $this->total / $this->limit ); + $this_page = ceil( ($this->start+1) / $this->limit ); + + $last=$this_page-1; + $next=$this_page+1; + + $start_loop = floor($this_page-$displayed_span); + $stop_loop = ceil($this_page + $displayed_span); + + + + $stopcredit =($start_loop<1)?0-$start_loop:0; + $startcredit =($stop_loop>$total_pages)?$stop_loop-$total_pages:0; + + $start_loop =($start_loop-$startcredit>0)?$start_loop-$startcredit:1; + $stop_loop =($stop_loop+$stopcredit>$total_pages)?$total_pages:$stop_loop+$stopcredit; + + if($start_loop>1){ + $lastspan=($start_loop-$displayed_span>0)?$start_loop-$displayed_span:1; + $html .= "\n<a href=\"$file&p=$lastspan\" ><strong>«</strong></a>"; + } + + for ($i=$start_loop; $i <= $stop_loop; $i++) { + $page = ($i - 1) * $this->limit; + if ($i == $this_page) { + $html .= "\n<b>[$i]</b>"; + } else { + $html .= "\n<a href=\"$file&p=$i\" ><b>$i</b></a>"; + } + } + if($stop_loop<$total_pages){ + $nextspan=($stop_loop+$displayed_span>$total_pages)?$total_pages-$displayed_span:$stop_loop+$displayed_span; + $html .= "\n<a href=\"$file&p=$nextspan\" ><strong>»</strong></a>"; + } + + + + return $html; + } + +} +?> diff --git a/include/class.passwd.php b/include/class.passwd.php new file mode 100644 index 0000000000000000000000000000000000000000..ef0cdac34d889961b672e7e3f5fda60a178ee854 --- /dev/null +++ b/include/class.passwd.php @@ -0,0 +1,43 @@ +<?php +/************************************************************************* + class.passwd.php + + Password Hasher - Interface for phpass bcrypt hasher. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once(INCLUDE_DIR.'PasswordHash.php'); //helper class - will be removed then we move to php5 + +define('DEFAULT_WORK_FACTOR',8); + +class Passwd { + + function cmp($passwd,$hash,$work_factor=0){ + + if($work_factor < 4 || $work_factor > 31) + $work_factor=DEFAULT_WORK_FACTOR; + + $hasher = new PasswordHash($work_factor,FALSE); + + return ($hasher && $hasher->CheckPassword($passwd,$hash)); + } + + function hash($passwd, $work_factor=0){ + + if($work_factor < 4 || $work_factor > 31) + $work_factor=DEFAULT_WORK_FACTOR; + + $hasher = new PasswordHash($work_factor,FALSE); + + return ($hasher && ($hash=$hasher->HashPassword($passwd)))?$hash:null; + } +} +?> diff --git a/include/class.pop3.php b/include/class.pop3.php new file mode 100644 index 0000000000000000000000000000000000000000..2ece6077c25bb2bb1784f3e9c7a2ddd2f4794e97 --- /dev/null +++ b/include/class.pop3.php @@ -0,0 +1,3 @@ +<?php +//No longer used. just used to clear the old file for now. Will be deleted in the upcoming versions. +?> diff --git a/include/class.priority.php b/include/class.priority.php new file mode 100644 index 0000000000000000000000000000000000000000..6fefe3671395c96aad075f9d67bd7a388cc584db --- /dev/null +++ b/include/class.priority.php @@ -0,0 +1,92 @@ +<?php +/********************************************************************* + class.priority.php + + Priority handle + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Priority { + + var $id; + var $ht; + + function Priority($id){ + + $this->id =0; + $this->load($id); + } + + function load($id) { + if(!$id && !($id=$this->getId())) + return false; + + + $sql='SELECT * FROM '.PRIORITY_TABLE + .' WHERE priority_id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht= db_fetch_array($res); + $this->id= $this->ht['priority_id']; + + return true;; + } + + function getId() { + return $this->id; + } + + function getTag() { + return $this->ht['priority']; + } + + function getDesc() { + return $this->ht['priority_desc']; + } + + function getColor() { + return $this->ht['priority_color']; + } + + function getUrgency() { + return $this->ht['priority_urgency']; + } + + function isPublic() { + return ($this->ht['ispublic']); + } + + /* ------------- Static ---------------*/ + function lookup($id) { + return ($id && is_numeric($id) && ($p=new Priority($id)) && $p->getId()==$id)?$p:null; + } + + function getPriorities( $publicOnly=false) { + + $priorities=array(); + $sql ='SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE; + if($publicOnly) + $sql.=' WHERE ispublic=1'; + + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id, $name)=db_fetch_row($res)) + $priorities[$id] = $name; + } + + return $priorities; + } + + function getPublicPriorities() { + return self::getPriorities(true); + } +} +?> diff --git a/include/class.sla.php b/include/class.sla.php new file mode 100644 index 0000000000000000000000000000000000000000..9ea62b0527fbc64ede3b94b51f6f9a1a9b91243a --- /dev/null +++ b/include/class.sla.php @@ -0,0 +1,159 @@ +<?php +/********************************************************************* + class.sla.php + + SLA + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class SLA { + + var $id; + + var $info; + + function SLA($id){ + $this->id=0; + $this->load($id); + } + + function load($id) { + + $sql='SELECT * FROM '.SLA_TABLE.' WHERE id='.db_input($id); + if(($res=db_query($sql)) && db_num_rows($res)) { + $info=db_fetch_array($res); + $this->id=$info['id']; + $this->info=$info; + return true; + } + return false; + } + + function reload() { + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->info['name']; + } + + function getGracePeriod(){ + return $this->info['grace_period']; + } + + function getNotes(){ + return $this->info['notes']; + } + + function getInfo(){ + return $this->info; + } + + function isActive(){ + return ($this->info['isactive']); + } + + function sendAlerts(){ + return (!$this->info['disable_overdue_alerts']); + } + + function priorityEscalation(){ + return ($this->info['enable_priority_escalation']); + } + + function update($vars,&$errors){ + if(SLA::save($this->getId(),$vars,$errors)){ + $this->reload(); + return true; + } + + return false; + } + + function delete(){ + global $cfg; + + if($cfg && $cfg->getDefaultSLAId()==$this->getId()) + return false; + + $id=$this->getId(); + $sql='DELETE FROM '.SLA_TABLE.' WHERE id='.db_input($id).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())){ + db_query('UPDATE '.DEPT_TABLE.' SET sla_id=0 WHERE sla_id='.db_input($id)); + db_query('UPDATE '.TOPIC_TABLE.' SET sla_id=0 WHERE sla_id='.db_input($id)); + db_query('UPDATE '.TICKET_TABLE.' SET sla_id=0 WHERE sla_id='.db_input($id)); + } + + return $num; + } + + /** static functions **/ + function create($vars,&$errors){ + return SLA::save(0,$vars,$errors); + } + + function getIdByName($name){ + + $sql='SELECT id FROM '.SLA_TABLE.' WHERE name='.db_input($name); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($sla= new SLA($id)) && $sla->getId()==$id)?$sla:null; + } + + function save($id,$vars,&$errors){ + + + if(!$vars['grace_period']) + $errors['grace_period']='Grace period required'; + elseif(!is_numeric($vars['grace_period'])) + $errors['grace_period']='Numeric value required (in hours)'; + + if(!$vars['name']) + $errors['name']='Name required'; + elseif(($sid=SLA::getIdByName($vars['name'])) && $sid!=$id) + $errors['name']='Name already exists'; + + if($errors) return false; + + $sql=' updated=NOW() '. + ',isactive='.db_input($vars['isactive']). + ',name='.db_input($vars['name']). + ',grace_period='.db_input($vars['grace_period']). + ',disable_overdue_alerts='.db_input(isset($vars['disable_overdue_alerts'])?1:0). + ',enable_priority_escalation='.db_input(isset($vars['enable_priority_escalation'])?1:0). + ',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.SLA_TABLE.' SET '.$sql.' WHERE id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update SLA. Internal error occurred'; + }else{ + $sql='INSERT INTO '.SLA_TABLE.' SET '.$sql.',created=NOW() '; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to add SLA. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.staff.php b/include/class.staff.php new file mode 100644 index 0000000000000000000000000000000000000000..bfb0b79d2735c32b2410845fba1f0f89490bf0ba --- /dev/null +++ b/include/class.staff.php @@ -0,0 +1,653 @@ +<?php +/********************************************************************* + class.staff.php + + Everything about staff. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.dept.php'); +include_once(INCLUDE_DIR.'class.team.php'); +include_once(INCLUDE_DIR.'class.group.php'); +include_once(INCLUDE_DIR.'class.passwd.php'); + +class Staff { + + var $ht; + var $id; + + var $dept; + var $teams; + var $stats; + + function Staff($var){ + $this->id =0; + return ($this->load($var)); + } + + function load($var=''){ + + if(!$var && !($var=$this->getId())) + return false; + + $sql='SELECT staff.*,grp.*,tz.offset as tz_offset,TIME_TO_SEC(TIMEDIFF(NOW(),IFNULL(staff.passwdreset,staff.created))) as passwd_change_sec '. + 'FROM '.STAFF_TABLE.' staff '. + 'LEFT JOIN '.GROUP_TABLE.' grp ON(grp.group_id=staff.group_id) '. + 'LEFT JOIN '.TIMEZONE_TABLE.' tz ON(tz.id=staff.timezone_id) '; + $sql.=sprintf('WHERE %s=%s',is_numeric($var)?'staff_id':'username',db_input($var)); + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return NULL; + + $this->ht=db_fetch_array($res); + $this->id = $this->ht['staff_id']; + $this->teams =$this->ht['teams']=$this->getTeams(); + + $this->teams=array(); + $this->stats=array(); + + return ($this->id); + } + + function reload(){ + return $this->load(); + } + + function getHastable() { + return $this->ht; + } + + function getInfo(){ + return $this->getHastable(); + } + + /*compares user password*/ + function check_passwd($password){ + + /*bcrypt based password match*/ + if(Passwd::cmp($password,$this->getPasswd())) + return true; + + /*Fall back to MD5 && force a password reset if it matches*/ + if(strlen($this->getPasswd()) && !strcmp($this->getPasswd(), MD5($password))) { + $this->forcePasswdRest(); + + return true; + } + + return false; + } + + function forcePasswdRest() { + return db_query('UPDATE '.STAFF_TABLE.' SET change_passwd=1 WHERE staff_id='.db_input($this->getId())); + } + + /* check if passwd reset is due. */ + function isPasswdResetDue(){ + global $cfg; + return ($cfg && $cfg->getPasswdResetPeriod() && $this->ht['passwd_change_sec']>($cfg->getPasswdResetPeriod()*30*24*60*60)); + } + + function isPasswdChangeDue() { + return $this->isPasswdResetDue(); + } + + function getTZoffset() { + return $this->ht['tz_offset']; + } + + function observeDaylight() { + return $this->ht['daylight_saving']?true:false; + } + + function getRefreshRate(){ + return $this->ht['auto_refresh_rate']; + } + + function getPageLimit() { + return $this->ht['max_page_size']; + } + + function getId(){ + return $this->id; + } + + function getEmail(){ + return $this->ht['email']; + } + + function getUserName(){ + return $this->ht['username']; + } + + function getPasswd(){ + return $this->ht['passwd']; + } + + function getName(){ + return ucfirst($this->ht['firstname'].' '.$this->ht['lastname']); + } + + function getFirstName(){ + return $this->ht['firstname']; + } + + function getLastName(){ + return $this->ht['lastname']; + } + + function getGroupId(){ + return $this->ht['group_id']; + } + + function getSignature(){ + return $this->ht['signature']; + } + + function getDefaultSignatureType() { + return $this->ht['default_signature_type']; + } + + function forcePasswdChange(){ + return ($this->ht['change_passwd']); + } + + function getDepts(){ + //Departments the user is allowed to access...based on the group they belong to + user's dept. + return array_filter(array_unique(array_merge(explode(',',$this->ht['dept_access']),array($this->dept_id)))); //Neptune help us + } + + function getDepartments() { + return $this->getDepts(); + } + + function getDeptId(){ + return $this->ht['dept_id']; + } + + function getDept(){ + + if(!$this->dept && $this->getDeptIf()) + $this->dept= Dept::lookup($this->getDeptId()); + + return $this->dept; + } + + + function isManager(){ + return (($dept=$this->getDept()) && $dept->getManagerId()==$this->getId()); + } + + function isStaff(){ + return TRUE; + } + + function isGroupActive(){ + return ($this->ht['group_enabled']); + } + + function isactive(){ + return ($this->ht['isactive']); + } + + function isVisible(){ + return ($this->ht['isvisible']); + } + + function onVacation(){ + return ($this->ht['onvacation']); + } + + function isAvailable() { + return ($this->isactive() && $this->isGroupActive() && !$this->onVacation()); + } + + function isAccessLimited(){ + return ($this->ht['assigned_only']); + } + + function isadmin(){ + return ($this->ht['isadmin']); + } + + function isTeamMember($teamId) { + return ($teamId && in_array($teamId,$this->getTeams())); + } + + function canAccessDept($deptId) { + return ($deptId && in_array($deptId,$this->getDepts()) && !$this->isAccessLimited()); + } + + function canCreateTickets(){ + return ($this->ht['can_create_tickets']); + } + + function canEditTickets(){ + return ($this->ht['can_edit_tickets']); + } + + function canDeleteTickets(){ + return ($this->ht['can_delete_tickets']); + } + + function canCloseTickets(){ + return ($this->ht['can_close_tickets']); + } + + function canAssignTickets() { + return ($this->ht['can_assign_tickets']); + } + + function canTransferTickets() { + return ($this->ht['can_transfer_tickets']); + } + + function canBanEmails() { + return ($this->ht['can_ban_emails']); + } + + function canManageTickets(){ + return ($this->isadmin() + || $this->canDeleteTickets() + || $this->canCloseTickets()); + } + + function canManagePremade(){ + return ($this->ht['can_manage_premade']); + } + + function canManageCannedResponses() { + return $this->canManagePremade(); + } + + function canManageFAQ(){ + return ($this->ht['can_manage_faq']); + } + + function canManageFAQs() { + return $this->canManageFAQ(); + } + + function showAssignedTickets(){ + return ($this->ht['show_assigned_tickets'] + && ($this->isAdmin() || $this->isManager())); + } + + function getTeams(){ + + if(!$this->teams){ + $sql='SELECT team_id FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId()); + if(($res=db_query($sql)) && db_num_rows($res)) + while(list($id)=db_fetch_row($res)) + $this->teams[] = $id; + } + + return $this->teams; + } + /* stats */ + + function resetStats() { + $this->stats = array(); + } + + /* returns staff's quick stats - used on nav menu...etc && warnings */ + function getTicketsStats() { + + if(!$this->stats['tickets']) + $this->stats['tickets'] = Ticket::getStaffStats($this); + + return $this->stats['tickets']; + } + + function getNumAssignedTickets() { + + return ($stats=$this->getTicketsStats())?$stats['assigned']:0; + } + + function getNumClosedTickets() { + return ($stats=$this->getTicketsStats())?$stats['closed']:0; + } + + //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues + function updateProfile($vars,&$errors){ + + $vars['firstname']=Format::striptags($vars['firstname']); + $vars['lastname']=Format::striptags($vars['lastname']); + $vars['signature']=Format::striptags($vars['signature']); + + if($this->getId()!=$vars['id']) + $errors['err']='Internal Error'; + + if(!$vars['firstname']) + $errors['firstname']='First name required'; + + if(!$vars['lastname']) + $errors['lastname']='Last name required'; + + if(!$vars['email'] || !Validator::is_email($vars['email'])) + $errors['email']='Valid email required'; + elseif(Email::getIdByEmail($vars['email'])) + $errors['email']='Already in-use as system email'; + elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$this->getId()) + $errors['email']='Email already in-use by another staff member'; + + if($vars['phone'] && !Validator::is_phone($vars['phone'])) + $errors['phone']='Valid number required'; + + if($vars['mobile'] && !Validator::is_phone($vars['mobile'])) + $errors['mobile']='Valid number required'; + + if($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd']){ + + if(!$vars['passwd1']) + $errors['passwd1']='New password required'; + elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) + $errors['passwd1']='Must be at least 6 characters'; + elseif($vars['passwd1'] && strcmp($vars['passwd1'],$vars['passwd2'])) + $errors['passwd2']='Password(s) do not match'; + + if(!$vars['cpasswd']) + $errors['cpasswd']='Current password required'; + elseif(!$this->check_passwd($vars['cpasswd'])) + $errors['cpasswd']='Invalid current password!'; + } + + if(!$vars['timezone_id']) + $errors['timezone_id']='Time zone required'; + + if($vars['default_signature_type']=='mine' && !$vars['signature']) + $errors['default_signature_type'] = "You don't have a signature"; + + if($errors) return false; + + $sql='UPDATE '.STAFF_TABLE.' SET updated=NOW() ' + .' ,firstname='.db_input($vars['firstname']) + .' ,lastname='.db_input($vars['lastname']) + .' ,email='.db_input($vars['email']) + .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"' + .' ,phone_ext='.db_input($vars['phone_ext']) + .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"' + .' ,signature='.db_input($vars['signature']) + .' ,timezone_id='.db_input($vars['timezone_id']) + .' ,daylight_saving='.db_input(isset($vars['daylight_saving'])?1:0) + .' ,show_assigned_tickets='.db_input(isset($vars['show_assigned_tickets'])?1:0) + .' ,max_page_size='.db_input($vars['max_page_size']) + .' ,auto_refresh_rate='.db_input($vars['auto_refresh_rate']) + .' ,default_signature_type='.db_input($vars['default_signature_type']); + + + if($vars['passwd1']) + $sql.=',change_passwd=0,passwdreset=NOW(),passwd='.db_input(Passwd::hash($vars['passwd1'])); + + $sql.=' WHERE staff_id='.db_input($this->getId()); + + //echo $sql; + + return (db_query($sql)); + } + + + function updateTeams($teams){ + + if($teams){ + foreach($teams as $k=>$id){ + $sql='INSERT IGNORE INTO '.TEAM_MEMBER_TABLE.' SET updated=NOW(),staff_id='.db_input($this->getId()).',team_id='.db_input($id); + db_query($sql); + } + } + $sql='DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId()); + if($teams) + $sql.=' AND team_id NOT IN('.implode(',',$teams).')'; + + db_query($sql); + + return true; + } + + function update($vars,&$errors) { + if(!$this->save($this->getId(),$vars,$errors)) + return false; + + $this->updateTeams($vars['teams']); + $this->reload(); + + return true; + } + + function delete(){ + global $thisstaff; + + if(!$thisstaff || !($id=$this->getId()) || $id==$thisstaff->getId()) + return 0; + + $sql='DELETE FROM '.STAFF_TABLE.' WHERE staff_id='.db_input($id).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())){ + // DO SOME HOUSE CLEANING + //Move remove any ticket assignments...TODO: send alert to Dept. manager? + db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE status=\'open\' AND staff_id='.db_input($id)); + //Cleanup Team membership table. + db_query('DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($id)); + } + + return $num; + } + + /**** Static functions ********/ + + function getStaffMembers($availableonly=false) { + + $sql='SELECT s.staff_id,CONCAT_WS(", ",s.lastname, s.firstname) as name ' + .' FROM '.STAFF_TABLE.' s '; + + if($availableonly) { + $sql.=' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) ' + .' WHERE s.isactive=1 AND s.onvacation=0'; + } + + $sql.=' ORDER BY s.lastname, s.firstname'; + $users=array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id, $name) = db_fetch_row($res)) + $users[$id] = $name; + } + + return $users; + } + + function getAvailableStaffMembers() { + return self::getStaffMembers(true); + } + + function getIdByUsername($username){ + + $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE username='.db_input($username); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id) = db_fetch_row($res); + + return $id; + } + function getIdByEmail($email){ + + $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE email='.db_input($email); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id) = db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($staff= new Staff($id)) && $staff->getId()==$id)?$staff:null; + } + + function login($username,$passwd,&$errors,$strike=true){ + global $cfg; + + + if($_SESSION['_staff']['laststrike']) { + if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) { + $errors['err']='You\'ve reached maximum failed login attempts allowed.'; + }else{ //Timeout is over. + //Reset the counter for next round of attempts after the timeout. + $_SESSION['_staff']['laststrike']=null; + $_SESSION['_staff']['strikes']=0; + } + } + + if(!$errors && ($user=new StaffSession($username)) && $user->getId() && $user->check_passwd($passwd)){ + //update last login && password reset stuff. + $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '; + if($user->isPasswdResetDue() && !$user->isAdmin()) + $sql.=',change_passwd=1'; + $sql.=' WHERE staff_id='.db_input($user->getId()); + db_query($sql); + //Now set session crap and lets roll baby! + $_SESSION['_staff']=array(); //clear. + $_SESSION['_staff']['userID']=$username; + $user->refreshSession(); //set the hash. + $_SESSION['TZ_OFFSET']=$user->getTZoffset(); + $_SESSION['daylight']=$user->observeDaylight(); + Sys::log(LOG_DEBUG,'Staff login',sprintf("%s logged in [%s]",$user->getUserName(),$_SERVER['REMOTE_ADDR'])); //Debug. + $sid=session_id(); //Current ID + session_regenerate_id(TRUE); + //Destroy old session ID - needed for PHP version < 5.1.0 TODO: remove when we move to php 5.3 as min. requirement. + if($session && is_object($session) && $sid) + $session->destroy($sid); + session_write_close(); + + return $user; + } + + //If we get to this point we know the login failed. + $_SESSION['_staff']['strikes']+=1; + if(!$errors && $_SESSION['_staff']['strikes']>$cfg->getStaffMaxLogins()) { + $errors['err']='Forgot your login info? Contact Admin.'; + $_SESSION['_staff']['laststrike']=time(); + $alert='Excessive login attempts by a staff member?'."\n". + 'Username: '.$_POST['username']."\n".'IP: '.$_SERVER['REMOTE_ADDR']."\n".'TIME: '.date('M j, Y, g:i a T')."\n\n". + 'Attempts #'.$_SESSION['_staff']['strikes']."\n".'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n"; + Sys::log(LOG_ALERT,'Excessive login attempts ('.$_POST['username'].')',$alert,($cfg->alertONLoginError())); + + }elseif($_SESSION['_staff']['strikes']%2==0){ //Log every other failed login attempt as a warning. + $alert='Username: '.$_POST['username']."\n".'IP: '.$_SERVER['REMOTE_ADDR']. + "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_staff']['strikes']; + Sys::log(LOG_WARNING,'Failed staff login attempt ('.$_POST['username'].')',$alert); + } + + return false; + } + + function create($vars,&$errors) { + if(($id=self::save(0,$vars,$errors)) && $vars['teams'] && ($self=Staff::lookup($id))) + $staff->updateTeams($vars['teams']); + + return $id; + } + + function save($id,$vars,&$errors) { + + $vars['username']=Format::striptags($vars['username']); + $vars['firstname']=Format::striptags($vars['firstname']); + $vars['lastname']=Format::striptags($vars['lastname']); + $vars['signature']=Format::striptags($vars['signature']); + + if($id && $id!=$vars['id']) + $errors['err']='Internal Error'; + + if(!$vars['firstname']) + $errors['firstname']='First name required'; + if(!$vars['lastname']) + $errors['lastname']='Last name required'; + + if(!$vars['username'] || strlen($vars['username'])<3) + $errors['username']='Username required'; + elseif(($uid=Staff::getIdByUsername($vars['username'])) && $uid!=$id) + $errors['username']='Username already in-use'; + + if(!$vars['email'] || !Validator::is_email($vars['email'])) + $errors['email']='Valid email required'; + elseif(Email::getIdByEmail($vars['email'])) + $errors['email']='Already in-use system email'; + elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$id) + $errors['email']='Email already in-use by another staff member'; + + if($vars['phone'] && !Validator::is_phone($vars['phone'])) + $errors['phone']='Valid number required'; + + if($vars['mobile'] && !Validator::is_phone($vars['mobile'])) + $errors['mobile']='Valid number required'; + + if($vars['passwd1'] || $vars['passwd2'] || !$id){ + if(!$vars['passwd1'] && !$id){ + $errors['passwd1']='Temp. password required'; + $errors['temppasswd']='Required'; + }elseif($vars['passwd1'] && strlen($vars['passwd1'])<6){ + $errors['passwd1']='Must be at least 6 characters'; + }elseif($vars['passwd1'] && strcmp($vars['passwd1'],$vars['passwd2'])){ + $errors['passwd2']='Password(s) do not match'; + } + } + + if(!$vars['dept_id']) + $errors['dept_id']='Department required'; + + if(!$vars['group_id']) + $errors['group_id']='Group required'; + + if(!$vars['timezone_id']) + $errors['timezone_id']='Time zone required'; + + if($errors) return false; + + + $sql=' SET updated=NOW() '. + ',isadmin='.db_input($vars['isadmin']). + ',isactive='.db_input($vars['isactive']). + ',isvisible='.db_input(isset($vars['isvisible'])?1:0). + ',onvacation='.db_input(isset($vars['onvacation'])?1:0). + ',assigned_only='.db_input(isset($vars['assigned_only'])?1:0). + ',dept_id='.db_input($vars['dept_id']). + ',group_id='.db_input($vars['group_id']). + ',timezone_id='.db_input($vars['timezone_id']). + ',username='.db_input($vars['username']). + ',firstname='.db_input($vars['firstname']). + ',lastname='.db_input($vars['lastname']). + ',email='.db_input($vars['email']). + ',phone="'.db_input(Format::phone($vars['phone']),false).'"'. + ',phone_ext='.db_input($vars['phone_ext']). + ',mobile="'.db_input(Format::phone($vars['mobile']),false).'"'. + ',signature='.db_input($vars['signature']). + ',notes='.db_input($vars['notes']); + + if($vars['passwd1']) + $sql.=',passwd='.db_input(Passwd::hash($vars['passwd1'])); + + if(isset($vars['change_passwd'])) + $sql.=',change_passwd=1'; + + if($id) { + $sql='UPDATE '.STAFF_TABLE.' '.$sql.' WHERE staff_id='.db_input($id); + if(db_query($sql) && db_affected_rows()) + return true; + + $errors['err']='Unable to update the user. Internal error occurred'; + }else{ + $sql='INSERT INTO '.STAFF_TABLE.' '.$sql.',created=NOW()'; + if(db_query($sql) && ($uid=db_insert_id())) + return $uid; + + $errors['err']='Unable to create user. Internal error'; + } + + return false; + } + + +} +?> diff --git a/include/class.sys.php b/include/class.sys.php new file mode 100644 index 0000000000000000000000000000000000000000..ab10cd99442fa640bd36a6a329b6cc27e18f1b7f --- /dev/null +++ b/include/class.sys.php @@ -0,0 +1,107 @@ +<?php +/************************************************************************* + class.sys.php + + System core helper. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once(INCLUDE_DIR.'class.config.php'); //Config helper + +define('LOG_WARN',LOG_WARNING); + +class Sys { + + var $loglevel=array(1=>'Error','Warning','Debug'); + + + //Load configuration info. + function getConfig() { + $cfg= new Config(1); + return ($cfg && $cfg->getId())?$cfg:null; + } + + + function alertAdmin($subject,$message,$log=false) { + global $cfg; + + //Set admin's email address + if(!$cfg || !($to=$cfg->getAdminEmail())) + $to=ADMIN_EMAIL; + + //Try getting the alert email. + $email=null; + if($cfg && !($email=$cfg->getAlertEmail())) + $email=$cfg->getDefaultEmail(); //will take the default email. + + if($email) { + $email->send($to,$subject,$message); + }else {//no luck - try the system mail. + Email::sendmail($to,$subject,$message,sprintf('"osTicket Alerts"<%s>',$to)); + } + + //log the alert? Watch out for loops here. + if($log && is_object($cfg)) { //if $cfg is not set then it means we don't have DB connection. + Sys::log(LOG_CRIT,$subject,$message,false); //Log the entry...and make sure no alerts are resent. + } + + } + + function log($priority,$title,$message,$alert=true) { + global $cfg; + + switch($priority){ //We are providing only 3 levels of logs. Windows style. + case LOG_EMERG: + case LOG_ALERT: + case LOG_CRIT: + case LOG_ERR: + $level=1; + if($alert) { + Sys::alertAdmin($title,$message); + } + break; + case LOG_WARN: + case LOG_WARNING: + //Warning... + $level=2; + break; + case LOG_NOTICE: + case LOG_INFO: + case LOG_DEBUG: + default: + $level=3; + //debug + } + //Save log based on system log level settings. + if($cfg && $cfg->getLogLevel()>=$level){ + $loglevel=array(1=>'Error','Warning','Debug'); + $sql='INSERT INTO '.SYSLOG_TABLE.' SET created=NOW(),updated=NOW() '. + ',title='.db_input($title). + ',log_type='.db_input($loglevel[$level]). + ',log='.db_input($message). + ',ip_address='.db_input($_SERVER['REMOTE_ADDR']); + //echo $sql; + mysql_query($sql); //don't use db_query to avoid possible loop. + } + } + + function purgeLogs(){ + global $cfg; + + if($cfg && ($gp=$cfg->getLogGraceperiod()) && is_numeric($gp)) { + $sql='DELETE FROM '.SYSLOG_TABLE.' WHERE DATE_ADD(created, INTERVAL '.$gp.' MONTH)<=NOW()'; + db_query($sql); + } + + } +} + +?> diff --git a/include/class.team.php b/include/class.team.php new file mode 100644 index 0000000000000000000000000000000000000000..7aada43a3b13a6529822ef8a764ba958b7f838d8 --- /dev/null +++ b/include/class.team.php @@ -0,0 +1,224 @@ +<?php +/********************************************************************* + class.team.php + + Teams + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Team { + + var $id; + var $ht; + + var $members; + + function Team($id){ + + return $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT team.*,count(member.staff_id) as members ' + .' FROM '.TEAM_TABLE.' team ' + .' LEFT JOIN '.TEAM_MEMBER_TABLE.' member USING(team_id) ' + .' WHERE team.team_id='.db_input($id) + .' GROUP BY team.team_id '; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['team_id']; + $this->members=array(); + + return $this->id; + } + + function reload(){ + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['name']; + } + + function getNumMembers(){ + return $this->ht['members']; + } + + function getMembers(){ + + if(!$this->members && $this->getNumMembers()){ + $sql='SELECT m.staff_id FROM '.TEAM_MEMBER_TABLE.' m ' + .'LEFT JOIN '.STAFF_TABLE.' s USING(staff_id) ' + .'WHERE m.team_id='.db_input($this->getId()).' AND s.staff_id IS NOT NULL ' + .'ORDER BY s.lastname, s.firstname'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id)=db_fetch_row($res)) + if(($staff= Staff::lookup($id))) + $this->members[]= $staff; + } + } + + return $this->members; + } + + function getLeadId(){ + return $this->ht['lead_id']; + } + + function getTeamLead(){ + if(!$this->lead && $this->getLeadId()) + $this->lead=Staff::lookup($this->getLeadId()); + + return $this->lead; + } + + function getLead(){ + return $this->getTeamLead(); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo(){ + return $this->getHashtable(); + } + + function isEnabled(){ + return ($this->ht['isenabled']); + } + + function isActive(){ + return $this->isEnabled(); + } + + function update($vars,&$errors) { + + //reset team lead if they're being deleted + if($this->getLeadId()==$vars['lead_id'] + && $vars['remove'] && in_array($this->getLeadId(),$vars['remove'])) + $vars['lead_id']=0; + + //Save the changes... + if(!Team::save($this->getId(),$vars,$errors)) + return false; + + //Delete staff marked for removal... + if($vars['remove']) { + $sql='DELETE FROM '.TEAM_MEMBER_TABLE + .' WHERE team_id='.db_input($this->getId()) + .' AND staff_id IN('.implode(',',$_POST['remove']).')'; + db_query($sql); + } + + //Reload. + $this->reload(); + + return true; + } + + /* ----------- Static function ------------------*/ + function lookup($id){ + return ($id && is_numeric($id) && ($team= new Team($id)) && $team->getId()==$id)?$team:null; + } + + + function getIdbyName($name){ + + $sql='SELECT team_id FROM '.TEAM_TABLE.' WHERE name='.db_input($name); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function getTeams( $availableOnly=false ) { + + $teams=array(); + $sql='SELECT team_id, name FROM '.TEAM_TABLE; + if($availableOnly) { + //Make sure the members are active...TODO: include group check!! + $sql='SELECT t.team_id, t.name, count(m.staff_id) as members ' + .' FROM '.TEAM_TABLE.' t ' + .' LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.team_id=t.team_id) ' + .' INNER JOIN '.STAFF_TABLE.' s ON(s.staff_id=m.staff_id AND s.isactive=1 AND onvacation=0) ' + .' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) ' + .' WHERE t.isenabled=1 ' + .' GROUP BY t.team_id ' + .' HAVING members>0' + .' ORDER by t.name '; + } + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id,$name)=db_fetch_row($res)) + $teams[$id] = $name; + } + + return $teams; + } + + function getActiveTeams() { + return self::getTeams(true); + } + + function create($vars,&$errors) { + return self::save(0,$vars,$errors); + } + + function save($id,$vars,&$errors) { + + if($id && $vars['id']!=$id) + $errors['err']='Missing or invalid team'; + + if(!$vars['name']) { + $errors['name']='Team name required'; + }elseif(strlen($vars['name'])<3) { + $errors['name']='Team name must be at least 3 chars.'; + }elseif(($tid=Team::getIdByName($vars['name'])) && $tid!=$id){ + $errors['name']='Team name already exists'; + } + + if($errors) return false; + + $sql='SET updated=NOW(),isenabled='.db_input($vars['isenabled']). + ',name='.db_input($vars['name']). + ',isenabled='.db_input($vars['isenabled']). + ',noalerts='.db_input(isset($vars['noalerts'])?$vars['noalerts']:0). + ',notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.TEAM_TABLE.' '.$sql.',lead_id='.db_input($vars['lead_id']).' WHERE team_id='.db_input($id); + if(db_query($sql) && db_affected_rows()) + return true; + + $errors['err']='Unable to update the team. Internal error'; + }else{ + $sql='INSERT INTO '.TEAM_TABLE.' '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create the team. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.template.php b/include/class.template.php new file mode 100644 index 0000000000000000000000000000000000000000..ef53d7ed59ff1c404e525f2d903107118b923709 --- /dev/null +++ b/include/class.template.php @@ -0,0 +1,392 @@ +<?php +/********************************************************************* + class.template.php + + Email Template + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Template { + + var $id; + var $ht; + + function Template($id){ + $this->id=0; + $this->load($id); + } + + function load($id) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT tpl.*,count(dept.tpl_id) as depts ' + .' FROM '.EMAIL_TEMPLATE_TABLE.' tpl ' + .' LEFT JOIN '.DEPT_TABLE.' dept USING(tpl_id) ' + .' WHERE tpl_id='.db_input($id); + + if(!($res=db_query($sql))|| !db_num_rows($res)) + return false; + + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['tpl_id']; + + return true; + } + + function reload() { + return $this->load($this->getId()); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['name']; + } + + function getNotes(){ + return $this->ht['notes']; + } + + function isEnabled() { + return ($this->ht['isactive']); + } + + function isActive(){ + return $this->isEnabled(); + } + + function isInUse(){ + global $cfg; + + return ($this->ht['depts'] || ($cfg && $this->getId()==$cfg->getDefaultTemplateId())); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function setStatus($status){ + + $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET updated=NOW(), isactive='.db_input($status?1:0) + .' WHERE tpl_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + function getMsgTemplate($name){ + + //TODO: Don't preload - do ondemand fetch! + $tpl=array(); + switch(strtolower($name)){ + case 'ticket_autoresp': + $tpl=array('subj'=>$this->ht['ticket_autoresp_subj'],'body'=>$this->ht['ticket_autoresp_body']); + break; + case 'msg_autoresp': + $tpl=array('subj'=>$this->ht['message_autoresp_subj'],'body'=>$this->ht['message_autoresp_body']); + break; + case 'ticket_notice': + $tpl=array('subj'=>$this->ht['ticket_notice_subj'],'body'=>$this->ht['ticket_notice_body']); + break; + case 'overlimit_notice': + $tpl=array('subj'=>$this->ht['ticket_overlimit_subj'],'body'=>$this->ht['ticket_overlimit_body']); + break; + case 'ticket_reply': + $tpl=array('subj'=>$this->ht['ticket_reply_subj'],'body'=>$this->ht['ticket_reply_body']); + break; + case 'ticket_alert': + $tpl=array('subj'=>$this->ht['ticket_alert_subj'],'body'=>$this->ht['ticket_alert_body']); + break; + case 'msg_alert': + $tpl=array('subj'=>$this->ht['message_alert_subj'],'body'=>$this->ht['message_alert_body']); + break; + case 'note_alert': + $tpl=array('subj'=>$this->ht['note_alert_subj'],'body'=>$this->ht['note_alert_body']); + break; + case 'assigned_alert': + $tpl=array('subj'=>$this->ht['assigned_alert_subj'],'body'=>$this->ht['assigned_alert_body']); + break; + case 'transfer_alert': + $tpl=array('subj'=>$this->ht['transfer_alert_subj'],'body'=>$this->ht['transfer_alert_body']); + break; + case 'overdue_alert': + $tpl=array('subj'=>$this->ht['ticket_overdue_subj'],'body'=>$this->ht['ticket_overdue_body']); + break; + default: + Sys::log(LOG_WARNING,'Template Fetch Error',"Unable to fetch '$name' template - id #".$this->getId()); + $tpl=array(); + } + + return $tpl; + } + + + function getNewTicketAlertMsgTemplate() { + return $this->getMsgTemplate('ticket_alert'); + } + + function getNewMessageAlertMsgTemplate() { + return $this->getMsgTemplate('msg_alert'); + } + + function getNewTicketNoticeMsgTemplate() { + return $this->getMsgTemplate('ticket_notice'); + } + + function getNewMessageAutorepMsgTemplate() { + return $this->getMsgTemplate('msg_autoresp'); + } + + function getAutoRespMsgTemplate() { + return $this->getMsgTemplate('ticket_autoresp'); + } + + function getReplyMsgTemplate() { + return $this->getMsgTemplate('ticket_reply'); + } + + function getOverlimitMsgTemplate() { + return $this->getMsgTemplate('overlimit_notice'); + } + + function getNoteAlertMsgTemplate() { + return $this->getMsgTemplate('note_alert'); + } + + function getTransferAlertMsgTemplate() { + return $this->getMsgTemplate('transfer_alert'); + } + + function getAssignedAlertMsgTemplate() { + return $this->getMsgTemplate('assigned_alert'); + } + + function getOverdueAlertMsgTemplate() { + return $this->getMsgTemplate('overdue_alert'); + } + + function updateMsgTemplate($vars, &$errors) { + + if(!($tpls=Template::message_templates()) || !$tpls[$vars['tpl']]) + $errors['tpl']='Unknown or invalid template'; + + if(!$vars['subj']) + $errors['subj']='Message subject required'; + + if(!$vars['body']) + $errors['body']='Message body required'; + + + if($errors) return false; + + $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET updated=NOW() '; + switch(strtolower($vars['tpl'])){ + case 'ticket_autoresp': + $sql.=',ticket_autoresp_subj='.db_input($vars['subj']).',ticket_autoresp_body='.db_input($vars['body']); + break; + case 'msg_autoresp': + $sql.=',message_autoresp_subj='.db_input($vars['subj']).',message_autoresp_body='.db_input($vars['body']); + break; + case 'ticket_notice': + $sql.=',ticket_notice_subj='.db_input($vars['subj']).',ticket_notice_body='.db_input($vars['body']); + break; + case 'overlimit_notice': + $sql.=',ticket_overlimit_subj='.db_input($vars['subj']).',ticket_overlimit_body='.db_input($vars['body']); + break; + case 'ticket_reply': + $sql.=',ticket_reply_subj='.db_input($vars['subj']).',ticket_reply_body='.db_input($vars['body']); + break; + case 'ticket_alert': + $sql.=',ticket_alert_subj='.db_input($vars['subj']).',ticket_alert_body='.db_input($vars['body']); + break; + case 'msg_alert': + $sql.=',message_alert_subj='.db_input($vars['subj']).',message_alert_body='.db_input($vars['body']); + break; + case 'note_alert': + $sql.=',note_alert_subj='.db_input($vars['subj']).',note_alert_body='.db_input($vars['body']); + break; + case 'assigned_alert': + $sql.=',assigned_alert_subj='.db_input($vars['subj']).',assigned_alert_body='.db_input($vars['body']); + break; + case 'transfer_alert': + $sql.=',transfer_alert_subj='.db_input($vars['subj']).',transfer_alert_body='.db_input($vars['body']); + break; + case 'overdue_alert': + $sql.=',ticket_overdue_subj='.db_input($vars['subj']).',ticket_overdue_body='.db_input($vars['body']); + break; + default: + $errors['tpl']='Unknown or invalid template'; + return false; + } + + $sql.=' WHERE tpl_id='.db_input($this->getId()); + + return (db_query($sql)); + + } + + function update($vars,&$errors) { + + if(!$vars['isactive'] && $this->isInUse()) + $errors['isactive']='Template in-use can not be disabled!'; + + if(!$this->save($this->getId(),$vars,$errors)) + return false; + + $this->reload(); + + return true; + } + + function enable(){ + return ($this->setStatus(1)); + } + + function disable(){ + return (!$this->isInUse() && $this->setStatus(0)); + } + + function delete(){ + global $cfg; + + if($this->isInUse() || $cfg->getDefaultTemplateId()==$this->getId()) + return 0; + + $sql='DELETE FROM '.EMAIL_TEMPLATE_TABLE.' WHERE tpl_id='.db_input($this->getId()).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())) { + //isInuse check is enough - but it doesn't hurt make sure deleted tpl is not in-use. + db_query('UPDATE '.DEPT_TABLE.' SET tpl_id=0 WHERE tpl_id='.db_input($this->getId())); + } + + return $num; + } + + /*** Static functions ***/ + function message_templates(){ + + //TODO: Make it database driven and dynamic + $messages=array('ticket_autoresp'=>array('name'=>'New Ticket Autoresponse', + 'desc'=>'Autoresponse sent to user, if enabled, on new ticket.'), + 'msg_autoresp'=>array('name'=>'New Message Auto-response', + 'desc'=>'Confirmation sent to user when a new message is appended to an existing ticket.'), + 'ticket_notice'=>array('name'=>'New Ticket Notice', + 'desc'=>'Notice sent to user, if enabled, on new ticket created by staff on their behalf (e.g phone calls).'), + 'overlimit_notice'=>array('name'=>'Over Limit Notice', + 'desc'=>'A one time notice sent, if enabled, when user has reached the maximum allowed open tickets.'), + 'ticket_reply'=>array('name'=>'Response/Reply Template', + 'desc'=>'Template used on ticket response/reply'), + 'ticket_alert'=>array('name'=>'New Ticket Alert', + 'desc'=>'Alert sent to staff, if enabled, on new ticket.'), + 'msg_alert'=>array('name'=>'New Message Alert', + 'desc'=>'Alert sent to staff, if enabled, when user replies to an existing ticket.'), + 'note_alert'=>array('name'=>'Internal Note Alert', + 'desc'=>'Alert sent to selected staff, if enabled, on new internal note.'), + 'assigned_alert'=>array('name'=>'Ticket Assignment Alert', + 'desc'=>'Alert sent to staff on ticket assignment.'), + 'transfer_alert'=>array('name'=>'Ticket Transfer Alert', + 'desc'=>'Alert sent to staff on ticket transfer.'), + 'overdue_alert'=>array('name'=>'Overdue Ticket Alert', + 'desc'=>'Alert sent to staff on stale or overdue tickets.') + ); + return $messages; + } + + + function create($vars,&$errors) { + + return Template::save(0,$vars,$errors); + } + + function getIdByName($name){ + $sql='SELECT tpl_id FROM '.EMAIL_TEMPLATE_TABLE.' WHERE name='.db_input($name); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($t= new Template($id)) && $t->getId()==$id)?$t:null; + } + + function save($id,$vars,&$errors) { + global $cfg; + + $tpl=null; + $vars['name']=Format::striptags(trim($vars['name'])); + + if($id && $id!=$vars['id']) + $errors['err']='Internal error. Try again'; + + if(!$vars['name']) + $errors['name']='Name required'; + elseif(($tid=Template::getIdByName($vars['name'])) && $tid!=$id) + $errors['name']='Template name already exists'; + + if(!$id && (!$vars['tpl_id'] || !($tpl=Template::lookup($vars['tpl_id'])))) + $errors['tpl_id']='Selection required'; + + if($errors) return false; + + $sql=' updated=NOW() ' + .' ,name='.db_input($vars['name']) + .' ,isactive='.db_input($vars['isactive']) + .' ,notes='.db_input($vars['notes']); + + if($id) { + $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET '.$sql.' WHERE tpl_id='.db_input($id); + if(db_query($sql)) + return true; + $errors['err']='Unable to update the template. Internal error occurred'; + }elseif($tpl && ($info=$tpl->getInfo())){ + + $sql='INSERT INTO '.EMAIL_TEMPLATE_TABLE.' SET '.$sql + .' ,created=NOW() ' + .' ,cfg_id='.db_input($cfg->getId()) + .' ,ticket_autoresp_subj='.db_input($info['ticket_autoresp_subj']) + .' ,ticket_autoresp_body='.db_input($info['ticket_autoresp_body']) + .' ,ticket_notice_subj='.db_input($info['ticket_notice_subj']) + .' ,ticket_notice_body='.db_input($info['ticket_notice_body']) + .' ,ticket_alert_subj='.db_input($info['ticket_alert_subj']) + .' ,ticket_alert_body='.db_input($info['ticket_alert_body']) + .' ,message_autoresp_subj='.db_input($info['message_autoresp_subj']) + .' ,message_autoresp_body='.db_input($info['message_autoresp_body']) + .' ,message_alert_subj='.db_input($info['message_alert_subj']) + .' ,message_alert_body='.db_input($info['message_alert_body']) + .' ,note_alert_subj='.db_input($info['note_alert_subj']) + .' ,note_alert_body='.db_input($info['note_alert_body']) + .' ,assigned_alert_subj='.db_input($info['assigned_alert_subj']) + .' ,assigned_alert_body='.db_input($info['assigned_alert_body']) + .' ,ticket_overdue_subj='.db_input($info['ticket_overdue_subj']) + .' ,ticket_overdue_body='.db_input($info['ticket_overdue_body']) + .' ,ticket_overlimit_subj='.db_input($info['ticket_overlimit_subj']) + .' ,ticket_overlimit_body='.db_input($info['ticket_overlimit_body']) + .' ,ticket_reply_subj='.db_input($info['ticket_reply_subj']) + .' ,ticket_reply_body='.db_input($info['ticket_reply_body']); + + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create template. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.ticket.php b/include/class.ticket.php new file mode 100644 index 0000000000000000000000000000000000000000..edb9ac4608cd6df114bebd62c798d4b205b443aa --- /dev/null +++ b/include/class.ticket.php @@ -0,0 +1,1949 @@ +<?php +/********************************************************************* + class.ticket.php + + The most important class! Don't play with fire please. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.staff.php'); +include_once(INCLUDE_DIR.'class.team.php'); +include_once(INCLUDE_DIR.'class.email.php'); +include_once(INCLUDE_DIR.'class.dept.php'); +include_once(INCLUDE_DIR.'class.topic.php'); +include_once(INCLUDE_DIR.'class.lock.php'); +include_once(INCLUDE_DIR.'class.file.php'); +include_once(INCLUDE_DIR.'class.attachment.php'); +include_once(INCLUDE_DIR.'class.banlist.php'); +include_once(INCLUDE_DIR.'class.template.php'); +include_once(INCLUDE_DIR.'class.priority.php'); + +class Ticket{ + + var $id; + var $extid; + var $email; + var $status; + var $created; + var $reopened; + var $updated; + var $lastrespdate; + var $lastmsgdate; + var $duedate; + var $priority; + var $priority_id; + var $fullname; + var $staff_id; + var $team_id; + var $dept_id; + var $topic_id; + var $dept_name; + var $subject; + var $helptopic; + var $overdue; + + var $lastMsgId; + + var $dept; //Dept obj + var $sla; // SLA obj + var $staff; //Staff obj + var $team; //Team obj + var $topic; //Topic obj + var $tlock; //TicketLock obj + + function Ticket($id){ + $this->id = 0; + $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + //TODO: delete helptopic field in ticket table. + + $sql='SELECT ticket.*, topic.topic as helptopic, lock_id, dept_name, priority_desc ' + .' ,count(attach.attach_id) as attachments ' + .' ,count(DISTINCT message.msg_id) as messages ' + .' ,count(DISTINCT response.response_id) as responses ' + .' ,count(DISTINCT note.note_id) as notes ' + .' FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' + .' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.priority_id) ' + .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) ' + .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TICKET_MESSAGE_TABLE.' message ON (ticket.ticket_id=message.ticket_id) ' + .' LEFT JOIN '.TICKET_RESPONSE_TABLE.' response ON (ticket.ticket_id=response.ticket_id) ' + .' LEFT JOIN '.TICKET_NOTE_TABLE.' note ON (ticket.ticket_id=note.ticket_id ) ' + .' WHERE ticket.ticket_id='.db_input($id) + .' GROUP BY ticket.ticket_id'; + + //echo $sql; + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + + $this->ht=db_fetch_array($res); + + $this->id = $this->ht['ticket_id']; + $this->extid = $this->ht['ticketID']; + + $this->email = $this->ht['email']; + $this->fullname = $this->ht['name']; + $this->status = $this->ht['status']; + $this->created = $this->ht['created']; + $this->reopened = $this->ht['reopened']; + $this->updated = $this->ht['updated']; + $this->duedate = $this->ht['duedate']; + $this->closed = $this->ht['closed']; + $this->lastmsgdate = $this->ht['lastmessagedate']; + $this->lastrespdate = $this->ht['lastresponsedate']; + + $this->lock_id = $this->ht['lock_id']; + $this->priority_id = $this->ht['priority_id']; + $this->priority = $this->ht['priority_desc']; + $this->staff_id = $this->ht['staff_id']; + $this->team_id = $this->ht['team_id']; + $this->dept_id = $this->ht['dept_id']; + $this->dept_name = $this->ht['dept_name']; + $this->sla_id = $this->ht['sla_id']; + $this->topic_id = $this->ht['topic_id']; + $this->helptopic = $this->ht['helptopic']; + $this->subject = $this->ht['subject']; + $this->overdue = $this->ht['isoverdue']; + + //Reset the sub classes (initiated ondemand)...good for reloads. + $this->staff = null; + $this->team = null; + $this->dept = null; + $this->sla = null; + $this->tlock = null; + $this->stats = null; + $this->topic = null; + + return true; + } + + function reload() { + return $this->load(); + } + + function isOpen() { + return (strcasecmp($this->getStatus(),'Open')==0); + } + + function isReopened() { + return ($this->getReopenDate()); + } + + function isClosed() { + return (strcasecmp($this->getStatus(),'Closed')==0); + } + + function isAssigned() { + return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId())); + } + + function isOverdue() { + return ($this->overdue); + } + + function isAnswered() { + return ($this->ht['isanswered']); + } + + function isLocked() { + return ($this->getLockId()); + } + + function checkStaffAccess($staff) { + + if(!is_object($staff) && !($staff=Staff::lookup($staff))) + return false; + + return ($staff->canAccessDept($this->getDeptId()) + || ($this->getTeamId() && $staff->isTeamMember($this->getTeamId())) + || $staff->getId()==$this->getStaffId()); + } + + //Getters + function getId(){ + return $this->id; + } + + function getExtId(){ + return $this->extid; + } + + function getEmail(){ + return $this->email; + } + + function getName(){ + return $this->fullname; + } + + function getSubject() { + return $this->subject; + } + + /* Help topic title - NOT object -> $topic */ + function getHelpTopic() { + + if(!$this->helpTopic && ($topic=$this->getTopic())) + $this->helpTopic = $topic->getName(); + + return $this->helptopic; + } + + function getCreateDate(){ + return $this->created; + } + + function getOpenDate() { + return $this->getCreateDate(); + } + + function getReopenDate() { + return $this->reopened; + } + + function getUpdateDate(){ + return $this->updated; + } + + function getDueDate(){ + return $this->duedate; + } + + function getCloseDate(){ + return $this->closed; + } + + function getStatus(){ + return $this->status; + } + + function getDeptId(){ + return $this->dept_id; + } + + function getDeptName(){ + return $this->dept_name; + } + + function getPriorityId() { + return $this->priority_id; + } + + function getPriority() { + return $this->priority; + } + + function getPhone() { + return $this->ht['phone']; + } + + function getPhoneExt() { + return $this->ht['phone_ext']; + } + + function getPhoneNumber() { + $phone=Format::phone($this->getPhone()); + if(($ext=$this->getPhoneExt())) + $phone.=" $ext"; + + return $phone; + } + + function getSource() { + return $this->ht['source']; + } + + function getIP() { + return $this->ht['ip_address']; + } + + function getLockId() { + return $this->lock_id; + } + + function getLock(){ + + if(!$this->tlock && $this->getLockId()) + $this->tlock= TicketLock::lookup($this->getLockId(),$this->getId()); + + return $this->tlock; + } + + function acquireLock($staffId, $lockTime) { + + if(!$staffId or !$lockTime) //Lockig disabled? + return null; + + //Check if the ticket is already locked. + if(($lock=$this->getLock()) && !$lock->isExpired()) { + if($lock->getStaffId()!=$staffId) //someone else locked the ticket. + return null; + + //Lock already exits...renew it + $lock->renew($lockTime); //New clock baby. + + return $lock; + } + //No lock on the ticket or it is expired + $this->tlock=null; //clear crap + $this->lock_id=TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock.. + //load and return the newly created lock if any! + return $this->getLock(); + } + + function getDept(){ + + if(!$this->dept && $this->getDeptId()) + $this->dept= Dept::lookup($this->getDeptId()); + + return $this->dept; + } + + function getStaffId(){ + return $this->staff_id; + } + + function getStaff(){ + + if(!$this->staff && $this->getStaffId()) + $this->staff= Staff::lookup($this->getStaffId()); + + return $this->staff; + } + + function getTeamId(){ + return $this->team_id; + } + + function getTeam(){ + + if(!$this->team && $this->getTeamId()) + $this->team = Team::lookup($this->getTeamId()); + + return $this->team; + } + + function getAssignee() { + + if($staff=$this->getStaff()) + return $staff->getName(); + + if($team=$this->getTeam()) + return $team->getName(); + + return ''; + } + + + function getTopicId(){ + return $this->topic_id; + } + + function getTopic() { + + if(!$this->topic && $this->getTopicId()) + $this->topic = Topic::lookup($this->getTopicId); + + return $this->topic; + } + + + function getSLAId() { + return $this->sla_id; + } + + function getSLA() { + + if(!$this->sla && $this->getSLAId()) + $this->sla = SLA::lookup($this->getSLAId); + + return $this->sla; + } + + function getLastRespondent() { + + $sql ='SELECT resp.staff_id ' + .' FROM '.TICKET_RESPONSE_TABLE.' resp ' + .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) ' + .' WHERE resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 ' + .' ORDER BY resp.created DESC LIMIT 1'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return null; + + list($id)=db_fetch_row($res); + + return Staff::lookup($id); + + } + + function getLastMessageDate() { + + if($this->lastmsgdate) + return $this->lastmsgdate; + + //for old versions...XXX: still needed???? + $sql='SELECT created FROM '.TICKET_MESSAGE_TABLE + .' WHERE ticket_id='.db_input($this->getId()) + .' ORDER BY created DESC LIMIT 1'; + if(($res=db_query($sql)) && db_num_rows($res)) + list($this->lastmsgdate)=db_fetch_row($res); + + return $this->lastmsgdate; + } + + function getLastMsgDate() { + return $this->getLastMessageDate(); + } + + function getLastResponseDate() { + + if($this->lastrespdate) + return $this->lastrespdate; + + $sql='SELECT created FROM '.TICKET_RESPONSE_TABLE + .' WHERE ticket_id='.db_input($this->getId()) + .' ORDER BY created DESC LIMIT 1'; + if(($res=db_query($sql)) && db_num_rows($res)) + list($this->lastrespdate)=db_fetch_row($res); + + return $this->lastrespdate; + } + + function getLastRespDate() { + return $this->getLastResponseDate(); + } + + + function getLastMsgId() { + return $this->lastMsgId; + } + + function getRelatedTicketsCount(){ + + $sql='SELECT count(*) FROM '.TICKET_TABLE.' WHERE email='.db_input($this->getEmail()); + return db_count($sql); + } + + function getThreadCount() { + return $this->getNumMessages() + $this->getNumResponses(); + } + + function getNumMessages() { + return $this->ht['messages']; + } + + function getNumResponses() { + return $this->ht['responses']; + } + + function getNumNotes() { + return $this->ht['notes']; + } + + function getNotes($order='') { + + if(!$order || !in_array($order, array('DESC','ASC'))) + $order='DESC'; + + $sql ='SELECT note.*, count(DISTINCT attach.attach_id) as attachments ' + .' FROM '.TICKET_NOTE_TABLE.' note ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (note.ticket_id=attach.ticket_id AND note.note_id=attach.ref_id AND ref_type="N") ' + .' WHERE note.ticket_id='.db_input($this->getId()) + .' GROUP BY note.note_id ' + .' ORDER BY note.created '.$order; + + $notes=array(); + if(($res=db_query($sql)) && db_num_rows($res)) + while($rec=db_fetch_array($res)) + $notes[]=$rec; + + return $notes; + } + + function getMessages() { + + $sql='SELECT msg.msg_id, msg.created, msg.message ' + .' ,count(DISTINCT attach.attach_id) as attachments, count( DISTINCT resp.response_id) as responses ' + .' FROM '.TICKET_MESSAGE_TABLE.' msg ' + .' LEFT JOIN '.TICKET_RESPONSE_TABLE. ' resp ON(resp.msg_id=msg.msg_id) ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (msg.ticket_id=attach.ticket_id AND msg.msg_id=attach.ref_id AND ref_type="M") ' + .' WHERE msg.ticket_id='.db_input($this->getId()) + .' GROUP BY msg.msg_id ' + .' ORDER BY msg.created DESC '; + + $messages=array(); + if(($res=db_query($sql)) && db_num_rows($res)) + while($rec=db_fetch_array($res)) + $messages[] = $rec; + + return $messages; + } + + function getResponses($msgId) { + + $sql='SELECT resp.*, count(DISTINCT attach.attach_id) as attachments ' + .' FROM '.TICKET_RESPONSE_TABLE. ' resp ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (resp.ticket_id=attach.ticket_id AND resp.response_id=attach.ref_id AND ref_type="R") ' + .' WHERE resp.ticket_id='.db_input($this->getId()) + .' GROUP BY resp.response_id ' + .' ORDER BY resp.created'; + + $responses=array(); + if(($res=db_query($sql)) && db_num_rows($res)) + while($rec= db_fetch_array($res)) + $responses[] = $rec; + + return $responses; + } + + function getAttachments($refId=0, $type=null) { + + if($refId && !$type) + return NULL; + + //XXX: inner join the file table instead? + $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name ' + .' FROM '.FILE_TABLE.' f ' + .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' + .' WHERE a.ticket_id='.db_input($this->getId()); + + if($refId) + $sql.=' AND a.ref_id='.db_input($refId); + + if($type) + $sql.=' AND a.ref_type='.db_input($type); + + $attachments = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) + $attachments[] = $rec; + } + + return $attachments; + } + + function getAttachmentsLinks($refId, $type, $separator=' ',$target='') { + + $str=''; + foreach($this->getAttachments($refId, $type) as $attachment ) { + /* The has here can be changed but must match validation in attachment.php */ + $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); + if($attachment['size']) + $size=sprintf('(<i>%s</i>)',Format::file_size($attachment['size'])); + + $str.=sprintf('<a class="Icon file" href="attachment.php?id=%d&h=%s" target="%s">%s</a>%s %s', + $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + } + + return $str; + } + + /* -------------------- Setters --------------------- */ + function setLastMsgId($msgid) { + return $this->lastMsgId=$msgid; + } + + function setPriority($priorityId) { + + //XXX: what happens to SLA priority??? + + if(!$priorityId || $priorityId==$this->getPriorityId()) + return ($priorityId); + + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' + .', priority_id='.db_input($priorityId) + .' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows($res)); + } + + //DeptId can NOT be 0. No orphans please! + function setDeptId($deptId){ + + //Make sure it's a valid department// + if(!($dept=Dept::lookup($deptId))) + return false; + + + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId) + .' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + //Set staff ID...assign/unassign/release (id can be 0) + function setStaffId($staffId){ + + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId) + .' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + function setSLAId($slaId) { + if ($slaId == $this->getSLAId()) return true; + return db_query( + 'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId) + .' WHERE ticket_id='.db_input($this->getId())) + && db_affected_rows(); + } + /** + * Selects the appropriate service-level-agreement plan for this ticket. + * When tickets are transfered between departments, the SLA of the new + * department should be applied to the ticket. This would be usefule, + * for instance, if the ticket is transferred to a different department + * which has a shorter grace period, the ticket should be considered + * overdue in the shorter window now that it is owned by the new + * department. + * + * $trump - if received, should trump any other possible SLA source. + * This is used in the case of email filters, where the SLA + * specified in the filter should trump any other SLA to be + * considered. + */ + function selectSLAId($trump=null) { + global $cfg; + # XXX Should the SLA be overwritten if it was originally set via an + # email filter? This method doesn't consider such a case + if ($trump !== null) { + $slaId = $trump; + } elseif ($this->getDept()->getSLAId()) { + $slaId = $this->getDept()->getSLAId(); + } elseif ($this->getTopicId() && $this->getTopic()) { + $slaId = $this->getTopic()->getSLAId(); + } else { + $slaId = $cfg->getDefaultSLAId(); + } + return ($slaId && $this->setSLAId($slaId)) ? $slaId : false; + } + + //Set team ID...assign/unassign/release (id can be 0) + function setTeamId($teamId){ + + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId) + .' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + //Status helper. + function setStatus($status) { + + if(strcasecmp($this->getStatus(),$status)==0) + return true; //No changes needed. + + switch(strtolower($status)) { + case 'open': + return $this->reopen(); + break; + case 'closed': + return $this->close(); + break; + } + + return false; + } + + function setState($state, $alerts=false) { + + switch(strtolower($state)) { + case 'open': + return $this->setStatus('open'); + break; + case 'closed': + return $this->setStatus('closed'); + break; + case 'answered': + return $this->setAnsweredState(1); + break; + case 'unanswered': + return $this->setAnsweredState(0); + break; + case 'overdue': + return $this->markOverdue(); + break; + } + + return false; + } + + + + + function setAnsweredState($isanswered) { + + $sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered) + .' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + //Close the ticket + function close(){ + global $thisstaff; + + $sql='UPDATE '.TICKET_TABLE.' SET closed=NOW(), isoverdue=0, duedate=NULL, updated=NOW(), status='.db_input('closed'); + + if($thisstaff) //Give the closing staff credit. + $sql.=', staff_id='.db_input($thisstaff->getId()); + + $sql.=' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + + //set status to open on a closed ticket. + function reopen($isanswered=0){ + + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), reopened=NOW() ' + .' ,status='.db_input('open') + .' ,isanswered='.db_input($isanswered) + .' WHERE ticket_id='.db_input($this->getId()); + + //TODO: log reopen event here + + return (db_query($sql) && db_affected_rows()); + } + + function onNewTicket($message, $autorespond=true, $alertstaff=true) { + global $cfg; + + //Log stuff here... + + if(!$autorespond && !$alertstaff) return true; //No alerts to send. + + /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/ + + $this->reload(); //get the new goodies. + $dept= $this->getDept(); + + if(!$dept || !($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + if(!$tpl) return false; //bail out...missing stuff. + + if(!$dept || !($email=$dept->getAutoRespEmail())) + $email =$cfg->getDefaultEmail(); + + //Send auto response - if enabled. + if($autorespond && $email && $cfg->autoRespONNewTicket() + && $dept->autoRespONNewTicket() + && ($msg=$tpl->getAutoRespMsgTemplate())) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%message', $message, $body); + $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body); + + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $body ="\n$tag\n\n".$body; + + //TODO: add auto flags....be nice to mail servers and sysadmins!! + $email->send($this->getEmail(),$subj,$body); + } + + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + //Send alert to out sleepy & idle staff. + if($alertstaff && $email + && $cfg->alertONNewTicket() + && ($msg=$tpl->getNewTicketAlertMsgTemplate())) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%message', $message, $body); + + $recipients=$sentlist=array(); + + //Alert admin?? + if($cfg->alertAdminONNewTicket()) { + $alert = str_replace("%staff",'Admin',$body); + $email->send($cfg->getAdminEmail(),$subj,$alert); + $sentlist[]=$cfg->getAdminEmail(); + } + + //Only alerts dept members if the ticket is NOT assigned. + if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) { + if(($members=$dept->getAvailableMembers())) + $recipients=array_merge($recipients, $members); + } + + if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager())) + $recipients[]= $manager; + + foreach( $recipients as $k=>$staff){ + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + $alert = str_replace("%staff",$staff->getFirstName(),$body); + $email->send($staff->getEmail(),$subj,$alert); + } + + + } + + return true; + } + + function onResponse(){ + db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1,lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); + } + + function onMessage($autorespond=true, $alert=true){ + global $cfg; + + db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId())); + + //auto-assign to closing staff or last respondent + if(!($staff=$this->getStaff()) || !$staff->isAvailable()) { + if($cfg->autoAssignReopenedTickets() && ($lastrep=$this->getLastRespondent()) && $lastrep->isAvailable()) { + $this->setStaffId($lastrep->getId()); //direct assignment; + } else { + $this->setStaffId(0); //unassign - last respondent is not available. + } + } + + if($this->isClosed()) $this->reopen(); //reopen.. + + /********** double check auto-response ************/ + if($autorespond && (Email::getIdByEmail($this->getEmail()))) + $autorespond=false; + elseif($autorespond && ($dept=$this->getDept())) + $autorespond=$dept->autoRespONNewMessage(); + + + if(!$autorespond && !$cfg->autoRespONNewMessage()) return; //no autoresp or alerts. + + $this->reload(); + + + if(!$dept && !($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + //If enabled...send confirmation to user. ( New Message AutoResponse) + if($tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body); + + //Reply separator tag. + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $body ="\n$tag\n\n".$body; + + if(!$dept || !($email=$dept->getAutoRespEmail())) + $email=$cfg->getDefaultEmail(); + + if($email) { + $email->send($this->getEMail(),$subj,$body); + } + } + + } + + function onAssign($note, $alert=true) { + global $cfg; + + if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign? + + $this->reload(); + + //Log an internal note - no alerts on the internal note. + $note=$note?$note:'Ticket assignment'; + $this->postNote('Ticket Assigned to '.$this->getAssignee(),$note,false); + + //See if we need to send alerts + if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! + + $dept = $this->getDept(); + + //Get template. + if(!$dept && !($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + //Email to use! + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + //Get the message template + if($tpl && ($msg=$tpl->getAssignedAlertMsgTemplate()) && $email) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%note', $note, $body); + $body = str_replace('%message', $note, $body); //Previous versions used message. + $body = str_replace('%assignee', $this->getAssignee(), $body); + $body = str_replace('%assigner', ($thisstaff)?$thisstaff->getName():'System',$body); + //recipients + $recipients=array(); + //Assigned staff or team... if any + // Assigning a ticket to a team when already assigned to staff disables alerts to the team (!)) + if($cfg->alertStaffONAssign() && $this->getStaffId()) + $recipients[]=$this->getStaff(); + elseif($this->getTeamId() && ($team=$this->getTeam())) { + if($cfg->alertTeamMembersOnAssignment() && ($members=$team->getMembers())) + $recipients+=$members; + elseif($cfg->alertTeamLeadOnAssignment() && ($lead=$team->getTeamLead())) + $recipients[]=$lead; + } + //Send the alerts. + $sentlist=array(); + foreach( $recipients as $k=>$staff){ + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + $alert = str_replace('%staff', $staff->getFirstName(), $body); + $email->send($staff->getEmail(), $subj, $alert); + } + print_r($sentlist); + } + + return true; + } + + function onOverdue($whine=true) { + global $cfg; + + // TODO: log overdue events here + + //check if we need to send alerts. + if(!$whine || !$cfg->alertONOverdueTicket()) + return true; + + + //Get template. + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + //Email to use! + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + //Get the message template + if($tpl && ($msg=$tpl->getOverdueAlertMsgTemplate()) && $email) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%comments', $comments, $body); //Planned support. + + //recipients + $recipients=array(); + //Assigned staff or team... if any + if($this->isAssigned() && $cfg->alertAssignedONTransfer()) { + if($this->getStaffId()) + $recipients[]=$this->getStaff(); + elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) + $recipients=array_merge($recipients, $members); + } elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) { + //Only alerts dept members if the ticket is NOT assigned. + if(($members=$dept->getAvailableMembers())) + $recipients=array_merge($recipients, $members); + } + //Always alert dept manager?? + if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager())) + $recipients[]= $manager; + + $sentlist=array(); + foreach( $recipients as $k=>$staff){ + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + $alert = str_replace("%staff",$staff->getFirstName(),$body); + $email->send($staff->getEmail(),$subj,$alert); + } + + } + + return true; + } + + //Replace base variables. + function replaceTemplateVars($text){ + global $cfg; + + $dept = $this->getDept(); + $staff= $this->getStaff(); + $team = $this->getTeam(); + + //TODO: add new vars (team, sla...etc) + + + $search = array('/%id/','/%ticket/','/%email/','/%name/','/%subject/','/%topic/','/%phone/','/%status/','/%priority/', + '/%dept/','/%assigned_staff/','/%createdate/','/%duedate/','/%closedate/','/%url/'); + $replace = array($this->getId(), + $this->getExtId(), + $this->getEmail(), + $this->getName(), + $this->getSubject(), + $this->getHelpTopic(), + $this->getPhoneNumber(), + $this->getStatus(), + $this->getPriority(), + ($dept?$dept->getName():''), + ($staff?$staff->getName():''), + Format::db_daydatetime($this->getCreateDate()), + Format::db_daydatetime($this->getDueDate()), + Format::db_daydatetime($this->getCloseDate()), + $cfg->getBaseUrl()); + return preg_replace($search,$replace,$text); + } + + + + + function markUnAnswered() { + return (!$this->isAnswered() || $this->setAnsweredState(0)); + } + + function markAnswered() { + return ($this->isAnswered() || $this->setAnsweredState(1)); + } + + function markOverdue($whine=true) { + + global $cfg; + + + if($this->isOverdue()) + return true; + + $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1,updated=NOW() ' + .' WHERE ticket_id='.db_input($this->getId()); + + if(!db_query($sql) || !db_affected_rows()) + return false; + + $this->onOverdue($whine); + + return true; + } + + //Dept Tranfer...with alert.. done by staff + function transfer($deptId, $comments, $alert = true) { + global $cfg, $thisstaff; + + if(!$this->setDeptId($deptId)) + return false; + + // Change to SLA of the new department + $this->selectSLAId(); + $currentDept = $this->getDeptName(); //XXX: add to olddept to tpl vars?? + + // Reopen ticket if closed + if($this->isClosed()) + $this->reopen(); + + $this->reload(); //reload - new dept!! + + //Send out alerts if enabled AND requested + if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!! + + + //Get template. + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + //Email to use! + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + //Get the message template + if($tpl && ($msg=$tpl->getTransferAlertMsgTemplate()) && $email) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%note', $comments, $body); + + //recipients + $recipients=array(); + //Assigned staff or team... if any + if($this->isAssigned() && $cfg->alertAssignedONTransfer()) { + if($this->getStaffId()) + $recipients[]=$this->getStaff(); + elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) + $recipients+=$members; + } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) { + //Only alerts dept members if the ticket is NOT assigned. + if(($members=$dept->getAvailableMembers())) + $recipients+=$members; + } + + //Always alert dept manager?? + if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager())) + $recipients[]= $manager; + + $sentlist=array(); + foreach( $recipients as $k=>$staff){ + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + $alert = str_replace("%staff",$staff->getFirstName(),$body); + $email->send($staff->getEmail(),$subj,$alert); + } + } + + return true; + } + + function assignToStaff($staff, $note, $alert=true) { + + if(!is_object($staff) && !($staff=Staff::lookup($staff))) + return false; + + if(!$this->setStaffId($staff->getId())) + return false; + + $this->onAssign($note, $alert); + + return true; + } + + function assignToTeam($team, $note, $alert=true) { + + if(!is_object($team) && !($team=Team::lookup($team))) + return false; + + if(!$this->setTeamId($team->getId())) + return false; + + //Clear - staff if it's a closed ticket + // staff_id is overloaded -> assigned to & closed by. + if($this->isClosed()) + $this->setStaffId(0); + + $this->onAssign($note, $alert); + + return true; + } + + //Assign ticket to staff or team - overloaded ID. + function assign($assignId, $note, $alert=true) { + global $thisstaff; + + $rv=0; + $id=preg_replace("/[^0-9]/", "",$assignId); + if($assignId[0]=='t') { + $rv=$this->assignToTeam($id, $note, $alert); + } elseif($assignId[0]=='s' || is_numeric($assignId)) { + $alert=($thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!! + //We don't care if a team is already assigned to the ticket - staff assignment takes precedence + $rv=$this->assignToStaff($id, $note, $alert); + } + + return $rv; + } + + //unassign primary assignee + function unassign() { + + if(!$this->isAssigned()) //We can't release what is not assigned buddy! + return true; + + //We're unassigning in the order of precedence. + if($this->getStaffId()) + return $this->setStaffId(0); + elseif($this->getTeamId()) + return $this->setTeamId(0); + + return false; + } + + function release() { + return $this->unassign(); + } + + //Insert message from client + function postMessage($msg,$source='',$msgid=NULL,$headers='',$newticket=false){ + global $cfg; + + if(!$this->getId()) return 0; + + # XXX: Refuse auto-response messages? (via email) XXX: No - but kill our auto-responder. + + $sql='INSERT INTO '.TICKET_MESSAGE_TABLE.' SET created=NOW() ' + .' ,ticket_id='.db_input($this->getId()) + .' ,messageId='.db_input($msgid) + .' ,message='.db_input(Format::striptags($msg)) //Tags/code stripped...meaning client can not send in code..etc + .' ,headers='.db_input($headers) //Raw header. + .' ,source='.db_input($source?$source:$_SERVER['REMOTE_ADDR']) + .' ,ip_address='.db_input($_SERVER['REMOTE_ADDR']); + + if(!db_query($sql) || !($msgid=db_insert_id())) return 0; //bail out.... + + $this->setLastMsgId($msgid); + + if($newticket) return $msgid; //Our work is done... + + $autorespond = true; + if ($autorespond && $headers && EmailFilter::isAutoResponse(Mail_Parse::splitHeaders($headers))) + $autorespond=false; + + $this->onMessage($autorespond); //must be called b4 sending alerts to staff. + + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + + //If enabled...send alert to staff (New Message Alert) + if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace("%message", $msg,$body); + + //Build list of recipients and fire the alerts. + $recipients=array(); + //Last respondent. + if($cfg->alertLastRespondentONNewMessage() || $cfg->alertAssignedONNewMessage()) + $recipients[]=$this->getLastRespondent(); + + //Assigned staff if any...could be the last respondent + + if($this->isAssigned() && ($staff=$this->getStaff())) + $recipients[]=$staff; + + //Dept manager + if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager())) + $recipients[]=$manager; + + $sentlist=array(); //I know it sucks...but..it works. + foreach( $recipients as $k=>$staff){ + if(!$staff || !$staff->getEmail() || !$staff->isAvailable() && in_array($staff->getEmail(),$sentlist)) continue; + $alert = str_replace("%staff",$staff->getFirstName(),$body); + $email->send($staff->getEmail(),$subj,$alert); + $sentlist[]=$staff->getEmail(); + } + } + + return $msgid; + } + + /* public */ + function postReply($vars, $files, $errors, $alert = true) { + global $thisstaff,$cfg; + + if(!$thisstaff || !$thisstaff->isStaff() || !$cfg) return 0; + + if(!$vars['msgId']) + $errors['msgId'] ='Missing messageId - internal error'; + if(!$vars['response']) + $errors['response'] = 'Resonse message required'; + + if($errors) return 0; + + $sql='INSERT INTO '.TICKET_RESPONSE_TABLE.' SET created=NOW() ' + .' ,ticket_id='.db_input($this->getId()) + .' ,msg_id='.db_input($vars['msgId']) + .' ,response='.db_input(Format::striptags($vars['response'])) + .' ,staff_id='.db_input($thisstaff->getId()) + .' ,staff_name='.db_input($thisstaff->getName()) + .' ,ip_address='.db_input($thisstaff->getIP()); + + if(!db_query($sql) || !($respId=db_insert_id())) + return false; + + //Set status - if checked. + if(isset($vars['reply_ticket_status']) && $vars['reply_ticket_status']) + $this->setStatus($vars['reply_ticket_status']); + + /* We can NOT recover from attachment related failures at this point */ + //upload files. + $attachments = $uploads = array(); + //Web based upload.. + if($files && is_array($files) && ($files=Format::files($files))) + $attachments=array_merge($attachments,$files); + + //Canned attachments... + if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) + $attachments=array_merge($attachments,$vars['cannedattachments']); + + + //Upload attachments -ids used on outgoing emails are returned. + if($attachments) + $uploads = $this->uploadAttachments($attachments, $respId,'R'); + + $this->onResponse(); //do house cleaning.. + $this->reload(); + $dept = $this->getDept(); + + /* email the user?? - if disabled - the bail out */ + if(!$alert) return $respId; + + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + if($tpl && ($msg=$tpl->getReplyMsgTemplate()) && $email) { + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%response',$vars['response'],$body); + + if($vars['signature']=='mine') + $signature=$thisstaff->getSignature(); + elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) + $signature=$dept->getSignature(); + else + $signature=''; + + $body = str_replace("%signature",$signature,$body); + + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $body ="\n$tag\n\n".$body; + + //Set attachments if emailing. + $attachments =($cfg->emailAttachments() && $uploads)?$this->getAttachments($respId,'R'):array(); + //TODO: setup 5 param (options... e.g mid trackable on replies) + $email->send($this->getEmail(), $subj, $body, $attachments); + } + + return $respId; + } + + //Activity log - saved as internal notes WHEN enabled!! + function logActivity($title,$note){ + global $cfg; + + if(!$cfg || !$cfg->logTicketActivity()) + return 0; + + return $this->postNote($title,$note,false,'system'); + } + + //Insert Internal Notes + function postNote($title,$note,$alert=true,$poster='') { + global $thisstaff,$cfg; + + $sql= 'INSERT INTO '.TICKET_NOTE_TABLE.' SET created=NOW() '. + ',ticket_id='.db_input($this->getId()). + ',title='.db_input(Format::striptags($title)). + ',note='.db_input(Format::striptags($note)). + ',staff_id='.db_input($thisstaff?$thisstaff->getId():0). + ',source='.db_input(($poster || !$thisstaff)?$poster:$thisstaff->getName()); + //echo $sql; + if(!db_query($sql) || !($id=db_insert_id())) + return false; + + // If alerts are not enabled then return a success. + if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept())) + return $id; + + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + if(!($email=$cfg->getAlertEmail())) + $email =$cfg->getDefaultEmail(); + + + if($tpl && ($msg=$tpl->getNoteAlertMsgTemplate()) && $email) { + + $body=$this->replaceTemplateVars($msg['body']); + $subj=$this->replaceTemplateVars($msg['subj']); + $body = str_replace('%note',"$title\n\n$note",$body); + + // Alert recipients + $recipients=array(); + + //Last respondent. + if($cfg->alertLastRespondentONNewNote()) + $recipients[]=$this->getLastRespondent(); + + //Assigned staff if any...could be the last respondent + if($cfg->alertAssignedONNewNote() && $this->isAssigned() && $this->getStaffId()) + $recipients[]=$this->getStaff(); + + //Dept manager + if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId()) + $recipients[]=$dept->getManager(); + + $sentlist=array(); + foreach( $recipients as $k=>$staff) { + if(!$staff || !is_object($staff) || !$staff->getEmail() || !$staff->isAvailable()) continue; + if(in_array($staff->getEmail(),$sentlist) || ($thisstaff && $thisstaff->getId()==$staff->getId())) continue; + $alert = str_replace('%staff',$staff->getFirstName(),$body); + $email->send($staff->getEmail(),$subj,$alert); + $sentlist[]=$staff->getEmail(); + } + } + + return $id; + } + + //online based attached files. + function uploadAttachments($files, $refid, $type) { + + $uploaded=array(); + foreach($files as $file) { + if(($fileId=is_numeric($file)?$file:AttachmentFile::upload($file)) && is_numeric($fileId)) + if($this->saveAttachment($fileId, $refid, $type)) + $uploaded[]=$fileId; + } + + return $uploaded; + } + + /* + Save attachment to the DB. uploads (above), email or json/xml. + + @file is a mixed var - can be ID or file hash. + */ + function saveAttachment($file, $refid, $type) { + + if(!$refid || !$type || !($fileId=is_numeric($file)?$file:AttachmentFile::save($file))) + return 0; + + $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' + .' ,ticket_id='.db_input($this->getId()) + .' ,file_id='.db_input($fileId) + .' ,ref_id='.db_input($refid) + .' ,ref_type='.db_input($type); + + return (db_query($sql) && ($id=db_insert_id()))?$id:0; + } + + + + function deleteAttachments(){ + global $cfg; + + $deleted=0; + if(($attachments = $this->getAttachments())) { + //Clear reference table - XXX: some attachments might be orphaned + db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getId())); + //Delete file from DB IF NOT inuse. + foreach($attachments as $attachment) { + if(($file=AttachmentFile::lookup($attachment['file_id'])) && !$file->isInuse() && $file->delete()) + $deleted++; + } + } + + return $deleted; + } + + + function delete(){ + + + if(db_query('DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1') && db_affected_rows()): + db_query('DELETE FROM '.TICKET_MESSAGE_TABLE.' WHERE ticket_id='.db_input($this->getId())); + db_query('DELETE FROM '.TICKET_RESPONSE_TABLE.' WHERE ticket_id='.db_input($this->getId())); + db_query('DELETE FROM '.TICKET_NOTE_TABLE.' WHERE ticket_id='.db_input($this->getId())); + $this->deleteAttachments(); + return TRUE; + endif; + + return FALSE; + } + + /*============== Static functions. Use Ticket::function(params); ==================*/ + function getIdByExtId($extid) { + $sql ='SELECT ticket_id FROM '.TICKET_TABLE.' ticket WHERE ticketID='.db_input($extid); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + + + function lookup($id){ //Assuming local ID is the only lookup used! + return ($id && is_numeric($id) && ($ticket= new Ticket($id)) && $ticket->getId()==$id)?$ticket:null; + } + + function genExtRandID() { + global $cfg; + + //We can allow collissions...extId and email must be unique ...so same id with diff emails is ok.. + // But for clarity...we are going to make sure it is unique. + $id=Misc::randNumber(EXT_TICKET_ID_LEN); + if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' WHERE ticketID='.db_input($id)))) + return Ticket::genExtRandID(); + + return $id; + } + + function getIdByMessageId($mid,$email) { + + if(!$mid || !$email) + return 0; + + $sql='SELECT ticket.ticket_id FROM '.TICKET_TABLE. ' ticket '. + ' LEFT JOIN '.TICKET_MESSAGE_TABLE.' msg USING(ticket_id) '. + ' WHERE messageId='.db_input($mid).' AND email='.db_input($email); + $id=0; + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function getOpenTicketsByEmail($email){ + + $sql='SELECT count(*) as open FROM '.TICKET_TABLE.' WHERE status='.db_input('open').' AND email='.db_input($email); + if(($res=db_query($sql)) && db_num_rows($res)) + list($num)=db_fetch_row($res); + + return $num; + } + + /* Quick staff's tickets stats */ + function getStaffStats($staff) { + global $cfg; + + /* Unknown or invalid staff */ + if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff()) + return null; + + + $sql='SELECT count(open.ticket_id) as open, count(answered.ticket_id) as answered ' + .' ,count(overdue.ticket_id) as overdue, count(assigned.ticket_id) as assigned, count(closed.ticket_id) as closed ' + .' FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.TICKET_TABLE.' open + ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\' AND open.isanswered=0) ' + .' LEFT JOIN '.TICKET_TABLE.' answered + ON (answered.ticket_id=ticket.ticket_id AND answered.status=\'open\' AND answered.isanswered=1) ' + .' LEFT JOIN '.TICKET_TABLE.' overdue + ON (overdue.ticket_id=ticket.ticket_id AND overdue.status=\'open\' AND overdue.isoverdue=1) ' + .' LEFT JOIN '.TICKET_TABLE.' assigned + ON (assigned.ticket_id=ticket.ticket_id AND assigned.status=\'open\' AND assigned.staff_id='.db_input($staff->getId()).')' + .' LEFT JOIN '.TICKET_TABLE.' closed + ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\' AND closed.staff_id='.db_input($staff->getId()).')' + .' WHERE (ticket.dept_id IN('.implode(',',$staff->getDepts()).') OR ticket.staff_id='.db_input($staff->getId()); + + + if(($teams=$staff->getTeams())) + $sql.=' OR ticket.team_id IN('.implode(',', array_filter($teams)).')'; + + + $sql.=')'; + + + if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets())) + $sql.=' AND (ticket.staff_id=0 OR ticket.staff_id='.db_input($staff->getId()).') '; + + + return db_fetch_array(db_query($sql)); + } + + function update($var,&$errors) { + global $cfg,$thisstaff; + + $fields=array(); + $fields['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); + $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Email is required'); + $fields['note'] = array('type'=>'text', 'required'=>1, 'error'=>'Reason for the update required'); + $fields['subject'] = array('type'=>'string', 'required'=>1, 'error'=>'Subject required'); + $fields['topicId'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Selection'); + $fields['pri'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Priority'); + $fields['phone'] = array('type'=>'phone', 'required'=>0, 'error'=>'Valid phone # required'); + $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY'); + + + $params = new Validator($fields); + if(!$params->validate($var)){ + $errors=array_merge($errors,$params->errors()); + } + + if($var['duedate']){ + if($this->isClosed()) + $errors['duedate']='Duedate can NOT be set on a closed ticket'; + elseif(!$var['time'] || strpos($var['time'],':')===false) + $errors['time']='Select time'; + elseif(strtotime($var['duedate'].' '.$var['time'])===false) + $errors['duedate']='Invalid duedate'; + elseif(strtotime($var['duedate'].' '.$var['time'])<=time()) + $errors['duedate']='Due date must be in the future'; + } + + //Make sure phone extension is valid + if($var['phone_ext'] ) { + if(!is_numeric($var['phone_ext']) && !$errors['phone']) + $errors['phone']='Invalid phone ext.'; + elseif(!$var['phone']) //make sure they just didn't enter ext without phone # + $errors['phone']='Phone number required'; + } + + $cleartopic=false; + $topicDesc=''; + if($var['topicId'] && ($topic= new Topic($var['topicId'])) && $topic->getId()) { + $topicDesc=$topic->getName(); + }elseif(!$var['topicId'] && $this->getTopicId()){ + $topicDesc=''; + $cleartopic=true; + } + + + if(!$errors){ + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() '. + ',email='.db_input($var['email']). + ',name='.db_input(Format::striptags($var['name'])). + ',subject='.db_input(Format::striptags($var['subject'])). + ',phone="'.db_input($var['phone'],false).'"'. + ',phone_ext='.db_input($var['phone_ext']?$var['phone_ext']:NULL). + ',priority_id='.db_input($var['pri']). + ',topic_id='.db_input($var['topicId']). + ',duedate='.($var['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($var['duedate'].' '.$var['time']))):'NULL'); + if($var['duedate']) { //We are setting new duedate... + $sql.=',isoverdue=0'; + } + if($topicDesc || $cleartopic) { //we're overwriting previous topic. + $sql.=',helptopic='.db_input($topicDesc); + } + $sql.=' WHERE ticket_id='.db_input($this->getId()); + //echo $sql; + if(db_query($sql)){ + $this->postNote('Ticket Updated',$var['note']); + $this->reload(); + return true; + } + } + + return false; + } + + + /* + * The mother of all functions...You break it you fix it! + * + * $autorespond and $alertstaff overwrites config info... + */ + function create($vars,&$errors, $origin, $autorespond=true, $alertstaff=true) { + global $cfg,$thisclient,$_FILES; + + //Make sure the email is not banned + if ($vars['email'] && EmailFilter::isBanned($vars['email'])) { + $errors['err']='Ticket denied. Error #403'; + Sys::log(LOG_WARNING,'Ticket denied','Banned email - '.$vars['email']); + return 0; + } + + $id=0; + $fields=array(); + $fields['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); + $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); + $fields['subject'] = array('type'=>'string', 'required'=>1, 'error'=>'Subject required'); + $fields['message'] = array('type'=>'text', 'required'=>1, 'error'=>'Message required'); + switch (strtolower($origin)) { + case 'web': + $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Select help topic'); + break; + case 'staff': + $fields['deptId'] = array('type'=>'int', 'required'=>1, 'error'=>'Dept. required'); + $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Topic required'); + $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY'); + case 'api': + $fields['source'] = array('type'=>'string', 'required'=>1, 'error'=>'Indicate source'); + break; + case 'email': + $fields['emailId'] = array('type'=>'int', 'required'=>1, 'error'=>'Email unknown'); + break; + default: + # TODO: Return error message + $errors['origin'] = 'Invalid origin given'; + } + $fields['pri'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Priority'); + $fields['phone'] = array('type'=>'phone', 'required'=>0, 'error'=>'Valid phone # required'); + + if(!Validator::process($fields, $vars, $errors) && !$errors['err']) + $errors['err'] ='Missing or invalid data - check the errors and try again'; + + //Make sure phone extension is valid + if($vars['phone_ext'] ) { + if(!is_numeric($vars['phone_ext']) && !$errors['phone']) + $errors['phone']='Invalid phone ext.'; + elseif(!$vars['phone']) //make sure they just didn't enter ext without phone # XXX: reconsider allowing! + $errors['phone']='Phone number required'; + } + + //Make sure the due date is valid + if($vars['duedate']){ + if(!$vars['time'] || strpos($vars['time'],':')===false) + $errors['time']='Select time'; + elseif(strtotime($vars['duedate'].' '.$vars['time'])===false) + $errors['duedate']='Invalid duedate'; + elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time()) + $errors['duedate']='Due date must be in the future'; + } + + //check attachment..if any is set ...only set on webbased tickets.. + //XXX:?? Create ticket anyway and simply drop the attachments?? We're already doing so with emails. + if($_FILES['attachment']['name'] && $cfg->allowOnlineAttachments()) { + if(!$cfg->canUploadFileType($_FILES['attachment']['name'])) + $errors['attachment']='Invalid file type [ '.Format::htmlchars($_FILES['attachment']['name']).' ]'; + elseif($_FILES['attachment']['size']>$cfg->getMaxFileSize()) + $errors['attachment']='File is too big. Max '.$cfg->getMaxFileSize().' bytes allowed'; + } + + # Perform email filter actions on the new ticket arguments XXX: Move filter to the top and check for reject... + if (!$errors && $ef = new EmailFilter($vars)) $ef->apply($vars); + + # Some things will need to be unpacked back into the scope of this + # function + if (isset($vars['autorespond'])) $autorespond=$vars['autorespond']; + + //check ticket limits..if limit set is >0 + //TODO: Base ticket limits on SLA... XXX: move it elsewhere?? + if($vars['email'] && !$errors && $cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff')){ + $openTickets=Ticket::getOpenTicketsByEmail($vars['email']); + if($openTickets>=$cfg->getMaxOpenTickets()) { + $errors['err']="You've reached the maximum open tickets allowed."; + //Send the notice only once (when the limit is reached) incase of autoresponders at client end. + if($cfg->getMaxOpenTickets()==$openTickets && $cfg->sendOverlimitNotice()) { + if($vars['deptId']) + $dept =Dept::lookup($vars['deptId']); + + if(!$dept || !($tpl=$dept->getTemplate())) + $tpl=$cfg->getDefaultTemplate(); + + if(!$dept || !($email=$dept->getAutoRespEmail())) + $email=$cfg->getDefaultEmail(); + + if($tpl && ($msg=$tpl->getOverlimitMsgTemplate()) && $email) { + $body = str_replace('%name', $vars['name'],$msg['body']); + $body = str_replace('%email',$vars['email'],$msg['body']); + $body = str_replace('%url', $cfg->getBaseUrl(),$body); + $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body); + $email->send($vars['email'],$msg['subj'],$body); + } + + //Log + Alert admin...this might be spammy (no option to disable)...but it is helpful..I think. + $msg='Support ticket request denied for '.$vars['email']."\n". + 'Open ticket:'.$openTickets."\n". + 'Max Allowed:'.$cfg->getMaxOpenTickets()."\n\nNotice only sent once"; + Sys::log(LOG_CRIT,'Overlimit Notice',$msg); + } + } + } + + //Any error above is fatal. + if($errors) return 0; + + // OK...just do it. + $deptId=$vars['deptId']; //pre-selected Dept if any. + $priorityId=$vars['pri']; + $source=ucfirst($vars['source']); + $topic=NULL; + // Intenal mapping magic...see if we need to overwrite anything + if(isset($vars['topicId']) && ($topic=Topic::lookup($vars['topicId']))) { //Ticket created via web by user/or staff + $deptId=$deptId?$deptId:$topic->getDeptId(); + $priorityId=$priorityId?$priorityId:$topic->getPriorityId(); + if($autorespond) $autorespond=$topic->autoRespond(); + $source=$vars['source']?$vars['source']:'Web'; + }elseif($vars['emailId'] && !$vars['deptId'] && ($email=Email::lookup($vars['emailId']))) { //Emailed Tickets + $deptId=$email->getDeptId(); + $priorityId=$priorityId?$priorityId:$email->getPriorityId(); + if($autorespond) $autorespond=$email->autoRespond(); + $email=null; + $source='Email'; + }elseif($vars['deptId']){ //Opened by staff. + $deptId=$vars['deptId']; + $source=ucfirst($vars['source']); + } + + //Last minute checks + $priorityId=$priorityId?$priorityId:$cfg->getDefaultPriorityId(); + $deptId=$deptId?$deptId:$cfg->getDefaultDeptId(); + $topicId=$vars['topicId']?$vars['topicId']:0; + $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR']; + + //We are ready son...hold on to the rails. + $extId=Ticket::genExtRandID(); + $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() ' + .' ,lastmessage= NOW()' + .' ,ticketID='.db_input($extId) + .' ,dept_id='.db_input($deptId) + .' ,topic_id='.db_input($topicId) + .' ,priority_id='.db_input($priorityId) + .' ,email='.db_input($vars['email']) + .' ,name='.db_input(Format::striptags($vars['name'])) + .' ,subject='.db_input(Format::striptags($vars['subject'])) + .' ,phone="'.db_input($vars['phone'],false).'"' + .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:'') + .' ,ip_address='.db_input($ipaddress) + .' ,source='.db_input($source); + + //Make sure the origin is staff - avoid firebug hack! + if($vars['duedate'] && !strcasecmp($origin,'staff')) + $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))); + + + if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id))) + return null; + + /* -------------------- POST CREATE ------------------------ */ + $dept = $ticket->getDept(); + + if(!$cfg->useRandomIds()){ + //Sequential ticketIDs support really..really suck arse. + $extId=$id; //To make things really easy we are going to use autoincrement ticket_id. + db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1'); + //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable] + } + + + //post the message. + $msgid=$ticket->postMessage($vars['message'],$source,$vars['mid'],$vars['header'],true); + //TODO: recover from postMessage error?? + + //Upload attachments...web based. - XXX: Assumes user uploaded attachments!! XXX: move it to client interface. + if($_FILES['attachment']['name'] && $cfg->allowOnlineAttachments() && $msgid) { + if(!$cfg->allowAttachmentsOnlogin() || ($cfg->allowAttachmentsOnlogin() && ($thisuser && $thisuser->isValid()))) { + $ticket->uploadAttachment($_FILES['attachment'],$msgid,'M'); + } + } + + // Configure service-level-agreement for this ticket + $ticket->selectSLAId($vars['slaId']); + + //Auto assign staff or team - auto assignment based on filter rules. + if($vars['staffId'] && !$vars['assignId']) + $ticket->assignToStaff($vars['staffId'],'auto-assignment'); + if($vars['teamId'] && !$vars['assignId']) + $ticket->assignToTeam($vars['teamId'],'auto-assignment'); + + /********** double check auto-response ************/ + //Overwrite auto responder if the FROM email is one of the internal emails...loop control. + if($autorespond && (Email::getIdByEmail($ticket->getEmail()))) + $autorespond=false; + + if($autorespond && $dept && !$dept->autoRespONNewTicket()) + $autorespond=false; + + # Messages that are clearly auto-responses from email systems should + # not have a return 'ping' message + if ($autorespond && $vars['header'] && + EmailFilter::isAutoResponse(Mail_Parse::splitHeaders($vars['header']))) { + $autorespond=false; + } + + //Don't auto respond to mailer daemons. + if( $autorespond && + (strpos(strtolower($vars['email']),'mailer-daemon@')!==false + || strpos(strtolower($vars['email']),'postmaster@')!==false)) { + $autorespond=false; + } + + /***** See if we need to send some alerts ****/ + + $ticket->onNewTicket($vars['message'], $autorespond, $alertstaff); + + return $ticket; + } + + function open($vars, $files, &$errors) { + global $thisstaff,$cfg; + + if(!$thisstaff || !$thisstaff->canCreateTickets()) return false; + + if(!$vars['issue']) + $errors['issue']='Summary of the issue required'; + else + $vars['message']=$vars['issue']; + + if($var['source'] && !in_array(strtolower($var['source']),array('email','phone','other'))) + $errors['source']='Invalid source - '.Format::htmlchars($var['source']); + + if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId'])))) + return false; + + $vars['msgId']=$ticket->getLastMsgId(); + $respId = 0; + + // post response - if any + if($vars['response']) { + $vars['response']=$ticket->replaceTemplateVars($vars['response']); + if(($respId=$ticket->postReply($vars, $files, $errors, false))) { + //Only state supported is closed on response + if(isset($vars['ticket_state']) && $thisstaff->canCloseTickets()) + $ticket->setState($vars['ticket_state']); + } + } + //Post Internal note + if($var['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team. + $ticket->assign($vars['assignId'],$vars['note']); + } elseif($vars['note']) { //Not assigned...save optional note if any + $ticket->postNote('New Ticket',$vars['note'],false); + } else { //Not assignment and no internal note - log activity + $ticket->logActivity('New Ticket by Staff','Ticket created by staff -'.$thisstaff->getName()); + } + + $ticket->reload(); + + if(!$cfg->notifyONNewStaffTicket() || !isset($var['alertuser'])) + return $ticket; //No alerts. + + //Send Notice to user --- if requested AND enabled!! + + $dept=$ticket->getDept(); + if(!$dept || !($tpl=$dept->getTemplate())) + $tpl=$cfg->getDefaultTemplate(); + + if(!$dept || !($email=$dept->getEmail())) + $email =$cfg->getDefaultEmail(); + + if($tpl && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && $email) { + + $message =$vars['issue']."\n\n".$vars['response']; + $body=$ticket->replaceTemplateVars($msg['body']); + $subj=$ticket->replaceTemplateVars($msg['subj']); + $body = str_replace('%message',$message,$body); + + if($vars['signature']=='mine') + $signature=$thisstaff->getSignature(); + elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) + $signature=$dept->getSignature(); + else + $signature=''; + + $body = str_replace('%signature',$signature,$body); + + if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator()))) + $body ="\n$tag\n\n".$body; + + $attachments =($cfg->emailAttachments() && $respId)?$this->getAttachments($respId,'R'):array(); + $email->send($ticket->getEmail(), $subj, $body, $attachments); + } + + return $ticket; + + } + + function checkOverdue(){ + + $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 JOIN '. + SLA_TABLE.' T2 ON T1.sla_id=T2.id '. + 'WHERE status=\'open\' AND isoverdue=0 '. + ' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=grace_period*3600)'. + ' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=grace_period*3600)'. + ' OR (duedate is NOT NULL AND duedate<NOW()) '. + ') ORDER BY T1.created LIMIT 50'; //Age upto 50 tickets at a time? + //echo $sql; + if(($stale=db_query($sql)) && db_num_rows($stale)){ + while(list($id)=db_fetch_row($stale)){ + if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue()) + $ticket->logActivity('Ticket Marked Overdue','Ticket flagged as overdue by the system.'); + # TODO: Send out notifications about the now-overdue + # ticket XXX: markOverdue sends out notifications. + } + } + } + +} +?> diff --git a/include/class.topic.php b/include/class.topic.php new file mode 100644 index 0000000000000000000000000000000000000000..59159c8c90a5501150f22342fdf886ed5a729330 --- /dev/null +++ b/include/class.topic.php @@ -0,0 +1,207 @@ +<?php +/********************************************************************* + class.topic.php + + Help topic helper + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class Topic { + var $id; + var $topic; + + var $ht; + + function Topic($id){ + $this->id=0; + $this->load($id); + } + + function load($id=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT * FROM '.TOPIC_TABLE + .' WHERE topic_id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht=db_fetch_array($res); + $this->id=$this->ht['topic_id']; + + return true; + } + + function reload() { + return $this->load(); + } + + function getId(){ + return $this->id; + } + + function getName(){ + return $this->ht['topic']; + } + + function getDeptId(){ + return $this->ht['dept_id']; + } + + function getSLAId(){ + return $this->ht['sla_id']; + } + + function getPriorityId(){ + return $this->ht['priority_id']; + } + + function getStaffId(){ + return $this->ht['staff_id']; + } + + function getTeamId(){ + return $this->ht['team_id']; + } + + function autoRespond() { + return (!$this->ht['noautoresp']); + } + + function isEnabled() { + return ($this->ht['isactive']); + } + + function isActive(){ + return $this->isEnabled(); + } + + function isPublic(){ + return ($this->ht['ispublic']); + } + + function getHashtable() { + return $this->ht; + } + + function getInfo() { + return $this->getHashtable(); + } + + function update($vars,&$errors) { + + if($this->save($this->getId(),$vars,$errors)){ + $this->reload(); + return true; + } + return false; + } + + function delete(){ + $sql='DELETE FROM '.TOPIC_TABLE.' WHERE topic_id='.db_input($this->getId()).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())){ + db_query('UPDATE '.TICKET_TABLE.' SET topic_id=0 WHERE topic_id='.db_input($this->getId())); + db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE topic_id='.db_input($this->getId())); + } + + return $num; + } + /*** Static functions ***/ + function create($vars,&$errors) { + return self::save(0,$vars,$errors); + } + + function getHelpTopics($publicOnly=false) { + + $topics=array(); + $sql='SELECT topic_id, topic FROM '.TOPIC_TABLE + .' WHERE isactive=1'; + if($publicOnly) + $sql.=' AND ispublic=1'; + $sql.=' ORDER BY topic'; + if(($res=db_query($sql)) && db_num_rows($res)) + while(list($id,$name)=db_fetch_row($res)) + $topics[$id]=$name; + + return $topics; + } + + + function getIdByName($topic){ + $sql='SELECT topic_id FROM '.TOPIC_TABLE.' WHERE topic='.db_input($topic); + if(($res=db_query($sql)) && db_num_rows($res)) + list($id)=db_fetch_row($res); + + return $id; + } + + function lookup($id){ + return ($id && is_numeric($id) && ($t= new Topic($id)) && $t->getId()==$id)?$t:null; + } + + function save($id,$vars,&$errors) { + + $vars['topic']=Format::striptags(trim($vars['topic'])); + + if($id && $id!=$vars['id']) + $errors['err']='Internal error. Try again'; + + if(!$vars['topic']) + $errors['topic']='Help topic required'; + elseif(strlen($vars['topic'])<5) + $errors['topic']='Topic is too short. 5 chars minimum'; + elseif(($tid=self::getIdByName($vars['topic'])) && $tid!=$id) + $errors['topic']='Topic already exists'; + + if(!$vars['dept_id']) + $errors['dept_id']='You must select a department'; + + if(!$vars['priority_id']) + $errors['priority_id']='You must select a priority'; + + if($errors) return false; + + $sql=' updated=NOW(),topic='.db_input($vars['topic']). + ',dept_id='.db_input($vars['dept_id']). + ',priority_id='.db_input($vars['priority_id']). + ',sla_id='.db_input($vars['sla_id']). + ',isactive='.db_input($vars['isactive']). + ',ispublic='.db_input($vars['ispublic']). + ',noautoresp='.db_input(isset($vars['noautoresp'])?1:0). + ',notes='.db_input($vars['notes']); + + //Auto assign ID is overloaded... + if($vars['assign'] && $vars['assign'][0]=='s') + $sql.=',team_id=0,staff_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign'])); + elseif($vars['assign'] && $vars['assign'][0]=='t') + $sql.=',staff_id=0,team_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign'])); + else + $sql.=',staff_id=0,team_id=0 '; //no auto-assignment! + + if($id) { + $sql='UPDATE '.TOPIC_TABLE.' SET '.$sql.' WHERE topic_id='.db_input($id); + if(db_query($sql)) + return true; + + $errors['err']='Unable to update topic. Internal error occurred'; + }else{ + $sql='INSERT INTO '.TOPIC_TABLE.' SET '.$sql.',created=NOW()'; + if(db_query($sql) && ($id=db_insert_id())) + return $id; + + $errors['err']='Unable to create the topic. Internal error'; + } + + return false; + } +} +?> diff --git a/include/class.usersession.php b/include/class.usersession.php new file mode 100644 index 0000000000000000000000000000000000000000..9a8283e3741a9b97f59a06649293831108634f93 --- /dev/null +++ b/include/class.usersession.php @@ -0,0 +1,181 @@ +<?php +/********************************************************************* + class.usersession.php + + User (client and staff) sessions handle. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +include_once(INCLUDE_DIR.'class.client.php'); +include_once(INCLUDE_DIR.'class.staff.php'); + + +class UserSession { + var $session_id = ''; + var $userID=''; + var $browser = ''; + var $ip = ''; + var $validated=FALSE; + + function UserSession($userid){ + + $this->browser=(!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT']; + $this->ip=(!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR'); + $this->session_id=session_id(); + $this->userID=$userid; + } + + function isStaff(){ + return FALSE; + } + + function isClient() { + return FALSE; + } + + + function getSessionId(){ + return $this->session_id; + } + + function getIP(){ + return $this->ip; + } + + function getBrowser(){ + return $this->browser; + } + function refreshSession(){ + //nothing to do...clients need to worry about it. + } + + function sessionToken(){ + + $time = time(); + $hash = md5($time.SESSION_SECRET.$this->userID); + $token = "$hash:$time:".MD5($this->ip); + + return($token); + } + + function isvalidSession($htoken,$maxidletime=0,$checkip=false){ + global $cfg; + + $token = rawurldecode($htoken); + + #check if we got what we expected.... + if($token && !strstr($token,":")) + return FALSE; + + #get the goodies + list($hash,$expire,$ip)=explode(":",$token); + + #Make sure the session hash is valid + if((md5($expire . SESSION_SECRET . $this->userID)!=$hash)){ + return FALSE; + } + #is it expired?? + + + if($maxidletime && ((time()-$expire)>$maxidletime)){ + return FALSE; + } + #Make sure IP is still same ( proxy access??????) + if($checkip && strcmp($ip, MD5($this->ip))) + return FALSE; + + $this->validated=TRUE; + + return TRUE; + } + + function isValid() { + return FALSE; + } + +} + +class ClientSession extends Client { + + var $session; + + function ClientSession($email,$id){ + parent::Client($email,$id); + $this->session= new UserSession($email); + } + + function isValid(){ + global $_SESSION,$cfg; + + if(!$this->getId() || $this->session->getSessionId()!=session_id()) + return false; + + return $this->session->isvalidSession($_SESSION['_client']['token'],$cfg->getClientTimeout(),false)?true:false; + } + + function refreshSession(){ + global $_SESSION; + $_SESSION['_client']['token']=$this->getSessionToken(); + //TODO: separate expire time from hash?? + } + + function getSession() { + return $this->session; + } + + function getSessionToken() { + return $this->session->sessionToken(); + } + + function getIP(){ + return $this->session->getIP(); + } +} + + +class StaffSession extends Staff { + + var $session; + + function StaffSession($var){ + parent::Staff($var); + $this->session= new UserSession($var); + } + + function isValid(){ + global $_SESSION,$cfg; + + if(!$this->getId() || $this->session->getSessionId()!=session_id()) + return false; + + return $this->session->isvalidSession($_SESSION['_staff']['token'],$cfg->getStaffTimeout(),$cfg->enableStaffIPBinding())?true:false; + } + + function refreshSession(){ + global $_SESSION; + $_SESSION['_staff']['token']=$this->getSessionToken(); + } + + function getSession() { + return $this->session; + } + + function getSessionToken() { + return $this->session->sessionToken(); + } + + function getIP(){ + return $this->session->getIP(); + } + +} + +?> diff --git a/include/class.validator.php b/include/class.validator.php new file mode 100644 index 0000000000000000000000000000000000000000..ea2dc62d0e079513940885b8f1734c4025bb178f --- /dev/null +++ b/include/class.validator.php @@ -0,0 +1,193 @@ +<?php +/********************************************************************* + class.validator.php + + Input validation helper. This class contains collection of functions used for data validation. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +class Validator { + + var $input=array(); + var $fields=array(); + var $errors=array(); + + function Validator($fields=null) { + $this->setFields($fields); + } + function setFields(&$fields){ + + if($fields && is_array($fields)): + $this->fields=$fields; + return (true); + endif; + + return (false); + } + + + function validate($source,$userinput=true){ + + $this->errors=array(); + //Check the input and make sure the fields are specified. + if(!$source || !is_array($source)) + $this->errors['err']='Invalid input'; + elseif(!$this->fields || !is_array($this->fields)) + $this->errors['err']='No fields setup'; + //Abort on error + if($this->errors) + return false; + + //if magic quotes are enabled - then try cleaning up inputs before validation... + if($userinput && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) + $source=Format::strip_slashes($source); + + + $this->input=$source; + + //Do the do. + foreach($this->fields as $k=>$field){ + if(!$field['required'] && !$this->input[$k]) //NOT required...and no data provided... + continue; + + if($field['required'] && !isset($this->input[$k]) || (!$this->input[$k] && $field['type']!='int')){ //Required...and no data provided... + $this->errors[$k]=$field['error']; + continue; + } + //Do the actual validation based on the type. + switch(strtolower($field['type'])): + case 'integer': + case 'int': + if(!is_numeric($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'double': + if(!is_numeric($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'text': + case 'string': + if(!is_string($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'array': + if(!$this->input[$k] || !is_array($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'radio': + if(!isset($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'date': //TODO...make sure it is really in GNU date format.. + if(strtotime($this->input[$k])===false) + $this->errors[$k]=$field['error']; + break; + case 'time': //TODO...make sure it is really in GNU time format.. + break; + case 'phone': + case 'fax': + if(!$this->is_phone($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'email': + if(!$this->is_email($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'url': + if(!$this->is_url($this->input[$k])) + $this->errors[$k]=$field['error']; + break; + case 'password': + if(strlen($this->input[$k])<5) + $this->errors[$k]=$field['error'].' (5 chars min)'; + break; + case 'username': + if(strlen($this->input[$k])<3) + $this->errors[$k]=$field['error'].' (3 chars min)'; + break; + case 'zipcode': + if(!is_numeric($this->input[$k]) || (strlen($this->input[$k])!=5)) + $this->errors[$k]=$field['error']; + break; + default://If param type is not set...or handle..error out... + $this->errors[$k]=$field['error'].' (type not set)'; + endswitch; + } + return ($this->errors)?(FALSE):(TRUE); + } + + function iserror(){ + return $this->errors?true:false; + } + + function errors(){ + return $this->errors; + } + + /*** Functions below can be called directly without class instance. Validator::func(var..); ***/ + function is_email($email) { + return (preg_match('/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i',trim(stripslashes($email)))); + } + function is_phone($phone) { + /* We're not really validating the phone number but just making sure it doesn't contain illegal chars and of acceptable len */ + $stripped=preg_replace("(\(|\)|\-|\+|[ ]+)","",$phone); + return (!is_numeric($stripped) || ((strlen($stripped)<7) || (strlen($stripped)>16)))?false:true; + } + + function is_url($url) { //Thanks to 4ice for the fix. + + + + $urlregex = "^(https?)\:\/\/"; + // USER AND PASS (optional) + $urlregex .= "([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?"; + // HOSTNAME OR IP + $urlregex .= "[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*"; // http://x = allowed (ex. http://localhost, http://routerlogin) + //$urlregex .= "[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)+"; // http://x.x = minimum + //$urlregex .= "([a-z0-9+\$_-]+\.)*[a-z0-9+\$_-]{2,3}"; // http://x.xx(x) = minimum + //use only one of the above + // PORT (optional) + $urlregex .= "(\:[0-9]{2,5})?"; + // PATH (optional) + $urlregex .= "(\/([a-z0-9+\$_-]\.?)+)*\/?"; + // GET Query (optional) + $urlregex .= "(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?"; + // ANCHOR (optional) + $urlregex .= "(#[a-z_.-][a-z0-9+\$_.-]*)?\$"; + + return eregi($urlregex, $url)?true:false; + } + + function is_ip($ip) { + + if(!$ip or empty($ip)) + return false; + + $ip=trim($ip); + if(preg_match("/^[0-9]{1,3}(.[0-9]{1,3}){3}$/",$ip)) { + foreach(explode(".", $ip) as $block) + if($block<0 || $block>255 ) + return false; + return true; + } + return false; + } + + function process($fields,$vars,&$errors){ + + $val = new Validator(); + $val->setFields($fields); + if(!$val->validate($vars)) + $errors=array_merge($errors,$val->errors()); + + return (!$errors); + } +} +?> diff --git a/include/class.xml.php b/include/class.xml.php new file mode 100644 index 0000000000000000000000000000000000000000..854f182372c94f57338b8b45a94d155f69f5bd33 --- /dev/null +++ b/include/class.xml.php @@ -0,0 +1,91 @@ +<?php +/********************************************************************* + class.xml.php + + Parses XML data input into a PHP array that looks something more like + JSON. Useful mainly for API xml requests. + + Jared Hancock + Copyright (c) 2006-2010 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: + $Id: $ +**********************************************************************/ + +class XmlDataParser { + + function XmlDataParser() { + $this->parser = xml_parser_create(); + xml_set_object($this->parser, $this); + xml_set_element_handler($this->parser, "startElement", "endElement"); + xml_set_character_data_handler($this->parser, "content"); + xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0); + xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1); + $this->content = array(); + $this->stack = array(); + } + + function parse($stream) { + while ($data = fread($stream, 4096)) { + if (!xml_parse($this->parser, $data, feof($stream))) + return false; + } + $this->content = $this->reduce($this->content); + return $this->content; + } + + function lastError() { + return sprintf("XML error: %s at line %d:%d", + xml_error_string(xml_get_error_code($this->parser)), + xml_get_current_line_number($this->parser), + xml_get_current_column_number($this->parser)); + } + /** + * Remove empty content and collapse simple elements (those that only + * have text content (not xml element content). + */ + function reduce($start) { + if (count($start) == 1 and isset($start[":text"])) { + # Collapse the :text to simple value for the key + return $start[":text"]; + } + else if (strlen($start[":text"]) == 0) { unset($start[":text"]); } + # Recurse + foreach ($start as $name => &$value) { + if (is_array($value)) $value = $this->reduce($value); + } + return $start; + } + + function startElement($parser, $name, $attrs) { + # Start a new array to hold upcoming contents. Push the current + # content onto a stack to become the "parent" content + array_push($this->stack, $this->content); + $this->content = array(":text" => ""); + foreach ($attrs as $name=>$value) + $this->content[$name] = $value; + } + + function endElement($parser, $name) { + # When an element is closed, associate the current content with the + # name of the element in the parent content array. + $prev = $this->content; + $this->content = array_pop($this->stack); + $i = 1; + if (array_key_exists($name, $this->content)) { + while (array_key_exists("$name$i", $this->content)) $i++; + $name = "$name$i"; + } + $this->content[$name] = $prev; + } + + function content($parser, $data) { + $this->content[":text"] .= trim($data); + } +} + +?> diff --git a/include/client/faq.inc.php b/include/client/faq.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..c944f8316f58fb1b1ff59854bb7a63c05d505de5 --- /dev/null +++ b/include/client/faq.inc.php @@ -0,0 +1,31 @@ +<?php +if(!defined('OSTCLIENTINC') || !$faq || !$faq->isPublished()) die('Access Denied'); + +$category=$faq->getCategory(); + +?> +<h1>Frequently Asked Questions</h1> +<div id="breadcrumbs"> + <a href="index.php">All Categories</a> + » <a href="faq.php?cid=<? echo $category->getId(); ?>"><? echo $category->getName(); ?></a> +</div> +<div style="width:700;padding-top:2px; float:left;"> +<strong style="font-size:16px;"><?php echo $faq->getQuestion() ?></strong> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"></div> +<div class="clear"></div> +<p> +<?php echo Format::safe_html($faq->getAnswer()); ?> +</p> +<p> +<?php +if($faq->getNumAttachments()) { ?> + <div><span class="faded"><b>Attachments:</b></span> <?php echo $faq->getAttachmentsLinks(); ?></div> +<? +}?> +<div><span class="faded"><b>Help Topics:</b></span> + <?php echo ($topics=$faq->getHelpTopics())?implode(', ',$topics):' '; ?> +</div> +</p> +<hr> +<div class="faded"> Last updated <?php echo Format::db_daydatetime($category->getUpdateDate()); ?></div> diff --git a/include/client/footer.inc.php b/include/client/footer.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..4b3d902c61a603f43e6a70a39fd44e469b1a7d6e --- /dev/null +++ b/include/client/footer.inc.php @@ -0,0 +1,8 @@ + </div> + </div> + <div id="footer"> + <p>Copyright © <?php echo date('Y'); ?> <a href="http://osticket.com" target="_blank" title="osTicket">osTicket.com</a> - All rights reserved.</p> + <a id="poweredBy" href="http://osticket.com" target="_blank">Powered by osTicket</a> + </div> +</body> +</html> diff --git a/include/client/header.inc.php b/include/client/header.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..ad88a0c80d13cb9655a5b113d28c5de94943b102 --- /dev/null +++ b/include/client/header.inc.php @@ -0,0 +1,58 @@ +<?php +$title=($cfg && is_object($cfg) && $cfg->getTitle())?$cfg->getTitle():'osTicket :: Support Ticket System'; +header("Content-Type: text/html; charset=UTF-8\r\n"); +?> +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + <title><?php echo Format::htmlchars($title); ?></title> + <meta name="description" content="customer support platform"> + <meta name="keywords" content="osTicket, Customer support system, support ticket system"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/theme.css" media="screen"> + <link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/print.css" media="print"> + <script src="./js/jquery.min.js"></script> + <script src="./js/osticket.js"></script> +</head> +<body> + <div id="container"> + <div id="header"> + <a id="logo" href="<?php echo ROOT_PATH; ?>index.php" title="Support Center"><img src="<?php echo ASSETS_PATH; ?>images/logo.png" border=0 alt="Support Center"></a> + <p> + <?php + if($thisclient && is_object($thisclient) && $thisclient->isValid()) { ?> + <a href="<?php echo ROOT_PATH; ?>tickets.php">My Tickets</a> - + <a href="<?php echo ROOT_PATH; ?>logout.php">Log Out</a> + <?php + }elseif($nav){ ?> + Guest User - <a href="<?php echo ROOT_PATH; ?>login.php">Log In</a> + <?php + } ?> + </p> + </div> + <?php + if($nav){ ?> + <ul id="nav"> + <?php + if($nav && ($navs=$nav->getNavLinks()) && is_array($navs)){ + foreach($navs as $name =>$nav) { + echo sprintf('<li><a class="%s %s" href="%s">%s</a></li>%s',$nav['active']?'active':'',$name,(ROOT_PATH.$nav['href']),$nav['desc'],"\n"); + } + } ?> + </ul> + <div id="content"> + <?php + }else{ ?> + <hr> + <div id="cnbg"> + <?php + } ?> + + <?php if($errors['err']) { ?> + <div id="msg_error"><?php echo $errors['err']; ?></div> + <?php }elseif($msg) { ?> + <div id="msg_notice"><?php echo $msg; ?></div> + <?php }elseif($warn) { ?> + <div id="msg_warning"><?php echo $warn; ?></div> + <?php } ?> diff --git a/include/client/kb-category.inc.php b/include/client/kb-category.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..f166fb5bdf8a1f528e56b965c5c32dca3e16dd8f --- /dev/null +++ b/include/client/kb-category.inc.php @@ -0,0 +1,35 @@ +<?php +if(!defined('OSTCLIENTINC') || !$category || !$category->isPublic()) die('Access Denied'); + +?> +<div style="width:700;padding-top:10px; float:left;"> + <h2>Frequently Asked Questions</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> </div> +<div class="clear"></div> +<br> +<div><strong><?php echo $category->getName() ?></strong></div> +<p> +<?php echo Format::safe_html($category->getDescription()); ?> +</p> +<hr> +<?php +$sql='SELECT faq.faq_id, question ' + .' FROM '.FAQ_TABLE.' faq ' + .' LEFT JOIN '.FAQ_ATTACHMENT_TABLE.' attach ON(attach.faq_id=faq.faq_id) ' + .' WHERE faq.ispublished=1 AND faq.category_id='.db_input($category->getId()) + .' GROUP BY faq.faq_id'; +if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<div id="faq"> + <ol>'; + while($row=db_fetch_array($res)) { + echo sprintf(' + <li><a href="faq.php?id=%d" >%s</a></li>', + $row['faq_id'],Format::htmlchars($row['question'])); + } + echo ' </ol> + </div>'; +}else { + echo '<strong>Category does not have any FAQs. <a href="index.php">Back To Index</a></strong>'; +} +?> diff --git a/include/client/knowledgebase.inc.php b/include/client/knowledgebase.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0af99ba21b1d92532a6063e175e9cf0fdc69c2c9 --- /dev/null +++ b/include/client/knowledgebase.inc.php @@ -0,0 +1,115 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); +?> +<h1>Frequently Asked Questions</h1> +<form action="index.php" method="get" style="padding-top:15px;"> + <input type="hidden" name="a" value="search"> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="440"> + <input id="query" type="text" size="20" name="q" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>"> + <select name="cid"> + <option value="">— All Categories —</option> + <?php + $sql='SELECT category_id, name, count(faq.category_id) as faqs ' + .' FROM '.FAQ_CATEGORY_TABLE.' cat ' + .' LEFT JOIN '.FAQ_TABLE.' faq USING(category_id) ' + .' GROUP BY cat.category_id ' + .' HAVING faqs>0 ' + .' ORDER BY cat.name DESC '; + if(($res=db_query($sql)) && db_num_rows($res)) { + while($row=db_fetch_array($res)) + echo sprintf('<option value="%d" %s>%s (%d)</option>', + $row['category_id'], + ($_REQUEST['cid'] && $row['category_id']==$_REQUEST['cid']?'selected="selected"':''), + $row['name'], + $row['faqs']); + } + ?> + </select> + </td> + <td width="100" rowspan="2" style="text-align:left;vertical-align: middle;"> + <input id="searchSubmit" type="submit" value="Search"> + </td> + </tr> + <tr> + <td width="400"> + <select name="topicId" style="width:350px;"> + <option value="">— All Help Topics —</option> + <?php + $sql='SELECT ht.topic_id, ht.topic, count(faq.topic_id) as faqs ' + .' FROM '.TOPIC_TABLE.' ht ' + .' LEFT JOIN '.FAQ_TOPIC_TABLE.' faq USING(topic_id) ' + .' GROUP BY ht.topic_id ' + .' HAVING faqs>0 ' + .' ORDER BY ht.topic DESC '; + if(($res=db_query($sql)) && db_num_rows($res)) { + while($row=db_fetch_array($res)) + echo sprintf('<option value="%d" %s>%s (%d)</option>', + $row['topic_id'], + ($_REQUEST['topicId'] && $row['topic_id']==$_REQUEST['topicId']?'selected="selected"':''), + $row['topic'], $row['faqs']); + } + ?> + </select> + </td> + </tr> + </table> +</form> +<hr> +<div> +<?php +if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. + $sql='SELECT faq.faq_id, question ' + .' FROM '.FAQ_TABLE.' faq ' + .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat USING(category_id) ' + .' WHERE faq.ispublished=1 AND cat.ispublic=1'; + if($_REQUEST['cid']) + $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']); + + if($_REQUEST['q']) + $sql.=" AND MATCH(question,answer,keywords) AGAINST ('".db_input($_REQUEST['q'],false)."')"; + + $sql.=' GROUP BY faq.faq_id'; + echo "<div><strong>Search Results</strong></div><div class='clear'></div>"; + if(($res=db_query($sql)) && ($num=db_num_rows($res))) { + echo '<div id="faq">'.$num.' FAQs matched your search criteria. + <ol>'; + while($row=db_fetch_array($res)) { + echo sprintf(' + <li><a href="faq.php?id=%d" class="previewfaq">%s</a></li>', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo ' </ol> + </div>'; + } else { + echo '<strong class="faded">The search did not match any FAQs.</strong>'; + } +} else { //Category Listing. + $sql='SELECT cat.category_id, cat.name, cat.description, cat.ispublic, count(faq.faq_id) as faqs ' + .' FROM '.FAQ_CATEGORY_TABLE.' cat ' + .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) ' + .' WHERE cat.ispublic=1 ' + .' GROUP BY cat.category_id ' + .' HAVING faqs>0 ' + .' ORDER BY cat.name'; + if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<div>Click on the category to browse FAQs.</div> + <ul id="kb">'; + while($row=db_fetch_array($res)) { + + echo sprintf(' + <li> + <h4><a href="faq.php?cid=%d">%s (%d)</a></h4> + %s + </li>',$row['category_id'], + Format::htmlchars($row['name']),$row['faqs'], + Format::safe_html($row['description'])); + } + echo '</ul>'; + } else { + echo 'NO FAQs found'; + } +} +?> +</div> diff --git a/include/client/login.inc.php b/include/client/login.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0ff10e3f41ea37e5f956b1b7add8877548e3f6b5 --- /dev/null +++ b/include/client/login.inc.php @@ -0,0 +1,26 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Kwaheri'); + +$email=Format::input($_POST['lemail']?$_POST['lemail']:$_GET['e']); +$ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']); +?> +<h1>Check Ticket Status</h1> +<p>To view the status of a ticket, provide us with the login details below.</p> +<form action="login.php" method="post" id="clientLogin"> + <strong>Authentication Required</strong> + <div> + <label for="email">E-Mail Address:</label> + <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>"> + </div> + <div> + <label for="ticketno">Ticket ID:</label> + <input id="ticketno" type="text" name="lticket" size="16" value="<?php echo $ticketid; ?>"></td> + </div> + <p> + <input class="btn" type="submit" value="View Status"> + </p> +</form> +<br> +<p> +If this is your first time contacting us or you've lost the ticket ID, please <a href="open.php">open a new ticket</a>. +</p> diff --git a/include/client/open.inc.php b/include/client/open.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..fd31bc9b81f907204134114e1aed59d3fc728cb6 --- /dev/null +++ b/include/client/open.inc.php @@ -0,0 +1,109 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); //Say bye to our friend.. + +$info=($_POST && $errors)?Format::htmlchars($_POST):array(); +?> + +<h1>Open a New Ticket</h1> +<p>Please fill in the form below to open a new ticket.</p> +<form id="ticketForm" method="post" action="open.php" enctype="multipart/form-data"> + <div> + <label for="name" class="required">Full Name:</label> + <input id="name" type="text" name="name" size="30" value="<?php echo $info['name']; ?>"> + <font class="error">* <?php echo $errors['name']; ?></font> + </div> + <div> + <label for="email" class="required">E-Mail Address:</label> + <input id="email" type="text" name="email" size="30" value="<?php echo $info['email']; ?>"> + <font class="error">* <?php echo $errors['email']; ?></font> + </div> + <div> + <label for="phone">Telephone:</label> + <input id="phone" type="text" name="phone" size="17" value="<?php echo $info['phone']; ?>"> + <label for="ext" class="inline">Ext.:</label> + <input id="ext" type="text" name="phone_ext" size="3" value="<?php echo $info['phone_ext']; ?>"> + <font class="error"> <?php echo $errors['phone']; ?> <?php echo $errors['phone_ext']; ?></font> + </div> + <br> + <div> + <label for="topicId" class="required">Help Topic:</label> + <select id="topicId" name="topicId"> + <option value="" selected="selected">— Select a Help Topics —</option> + <?php + $sql='SELECT topic_id,topic FROM '.TOPIC_TABLE.' WHERE isactive=1 ORDER BY topic'; + if(($res=db_query($sql)) && db_num_rows($res)) { + while (list($topicId,$topic) = db_fetch_row($res)){ + $selected = ($info['topicId']==$topicId)?'selected="selected"':''; ?> + <option value="<?php echo $topicId; ?>"<?php echo $selected; ?>><?php echo $topic; ?></option> + <?php + } + }else{ ?> + <option value="0" >General Inquiry</option> + <?php } ?> + </select> + <font class="error">* <?php echo $errors['topicId']; ?></font> + </div> + <div> + <label for="subject" class="required">Subject:</label> + <input id="subject" type="text" name="subject" size="40" value="<?php echo $info['subject']; ?>"> + <font class="error">* <?php echo $errors['subject']; ?></font> + </div> + <div> + <label for="msg" class="required">Message:</label> + <span id="msg"> + <em>Please provide as much details as possible so we can best assist you.</em> <font class="error">* <?php echo $errors['message']; ?></font></span> + </div> + <div> + <label for="message" class="required"> </label> + <textarea id="message" cols="60" rows="8" name="message"><?php echo $info['message']; ?></textarea> + </div> + <?php if(($cfg->allowOnlineAttachments() && !$cfg->allowAttachmentsOnlogin()) + || ($cfg->allowAttachmentsOnlogin() && ($thisuser && $thisuser->isValid()))) { ?> + <div> + <label for="attachment">Attachments:</label> + <input id="attachment" type="file" name="attachment"><font class="error"> <?php echo $errors['attachment']; ?></font> + </div> + <?php } ?> + <?php + if($cfg && $cfg->allowPriorityChange()) { + $sql='SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE.' WHERE ispublic=1 ORDER BY priority_urgency DESC'; + if(($res=db_query($sql)) && db_num_rows($res)) {?> + <div> + <label for="priority">Ticket Priority:</label> + <select id="priority" name="priorityId"> + <?php + if(!$info['priorityId']) + $info['priorityId']=$cfg->getDefaultPriorityId(); //use system's default priority. + while($row=db_fetch_array($res)){ + $selected=$info['priorityId']==$row['priority_id']?'selected="selected"':''; + ?> + <option value="<?php echo $row['priority_id']; ?>" <?php echo $selected; ?> ><?php echo $row['priority_desc']; ?></option> + <?php } ?> + </select> + <font class="error"> <?php echo $errors['priorityId']; ?></font> + </div> + <?php + } + } ?> + <?php + if($cfg && $cfg->enableCaptcha() && (!$thisuser || !$thisuser->isValid())) { + if($_POST && $errors && !$errors['captcha']) + $errors['captcha']='Please re-enter the text again'; + ?> + <br> + <div class="captchaRow"> + <label for="captcha" class="required">CAPTCHA Text:</label> + <span class="captcha"><img src="captcha.php" border="0" align="left"></span> + <input id="captcha" type="text" name="captcha" size="6"> + <em>Enter the text shown on the image.</em> + <font class="error">* <?php echo $errors['captcha']; ?></font> + </div> + <?php + } ?> + <br> + <p> + <input type="submit" value="Create Ticket"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" onClick='window.location.href="index.php"'> + </p> +</form> diff --git a/include/client/thankyou.inc.php b/include/client/thankyou.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..6e2fe3b4b7218ec1468d09ec71fd1b70624cd70f --- /dev/null +++ b/include/client/thankyou.inc.php @@ -0,0 +1,20 @@ +<?php +if(!defined('OSTCLIENTINC') || !is_object($ticket)) die('Kwaheri rafiki!'); +//Please customize the message below to fit your organization speak! +?> +<div style="margin:5px 100px 100px 0;"> + <?php echo Format::htmlchars($ticket->getName()); ?>,<br> + <p> + Thank you for contacting us.<br> + A support ticket request has been created and a representative will be getting back to you shortly if necessary.</p> + + <?php if($cfg->autoRespONNewTicket()){ ?> + <p>An email with the ticket number has been sent to <b><?php echo $ticket->getEmail(); ?></b>. + You'll need the ticket number along with your email to view status and progress online. + </p> + <p> + If you wish to send additional comments or information regarding same issue, please follow the instructions on the email. + </p> + <?php } ?> + <p>Support Team </p> +</div> diff --git a/include/htmLawed.php b/include/htmLawed.php new file mode 100644 index 0000000000000000000000000000000000000000..28d234824fb4765e4e60544774c64732ef0d4e67 --- /dev/null +++ b/include/htmLawed.php @@ -0,0 +1,711 @@ +<?php + +/* +htmLawed 1.1.10, 22 October 2011 +Copyright Santosh Patnaik +LGPL v3 license +A PHP Labware internal utility; www.bioinformatics.org/phplabware/internal_utilities/htmLawed + +See htmLawed_README.txt/htm +*/ + +function htmLawed($t, $C=1, $S=array()){ +$C = is_array($C) ? $C : array(); +if(!empty($C['valid_xhtml'])){ + $C['elements'] = empty($C['elements']) ? '*-center-dir-font-isindex-menu-s-strike-u' : $C['elements']; + $C['make_tag_strict'] = isset($C['make_tag_strict']) ? $C['make_tag_strict'] : 2; + $C['xml:lang'] = isset($C['xml:lang']) ? $C['xml:lang'] : 2; +} +// config eles +$e = array('a'=>1, 'abbr'=>1, 'acronym'=>1, 'address'=>1, 'applet'=>1, 'area'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'blockquote'=>1, 'br'=>1, 'button'=>1, 'caption'=>1, 'center'=>1, 'cite'=>1, 'code'=>1, 'col'=>1, 'colgroup'=>1, 'dd'=>1, 'del'=>1, 'dfn'=>1, 'dir'=>1, 'div'=>1, 'dl'=>1, 'dt'=>1, 'em'=>1, 'embed'=>1, 'fieldset'=>1, 'font'=>1, 'form'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'i'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'ins'=>1, 'isindex'=>1, 'kbd'=>1, 'label'=>1, 'legend'=>1, 'li'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'object'=>1, 'ol'=>1, 'optgroup'=>1, 'option'=>1, 'p'=>1, 'param'=>1, 'pre'=>1, 'q'=>1, 'rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1, 'ruby'=>1, 's'=>1, 'samp'=>1, 'script'=>1, 'select'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'table'=>1, 'tbody'=>1, 'td'=>1, 'textarea'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1, 'tt'=>1, 'u'=>1, 'ul'=>1, 'var'=>1); // 86/deprecated+embed+ruby +if(!empty($C['safe'])){ + unset($e['applet'], $e['embed'], $e['iframe'], $e['object'], $e['script']); +} +$x = !empty($C['elements']) ? str_replace(array("\n", "\r", "\t", ' '), '', $C['elements']) : '*'; +if($x == '-*'){$e = array();} +elseif(strpos($x, '*') === false){$e = array_flip(explode(',', $x));} +else{ + if(isset($x[1])){ + preg_match_all('`(?:^|-|\+)[^\-+]+?(?=-|\+|$)`', $x, $m, PREG_SET_ORDER); + for($i=count($m); --$i>=0;){$m[$i] = $m[$i][0];} + foreach($m as $v){ + if($v[0] == '+'){$e[substr($v, 1)] = 1;} + if($v[0] == '-' && isset($e[($v = substr($v, 1))]) && !in_array('+'. $v, $m)){unset($e[$v]);} + } + } +} +$C['elements'] =& $e; +// config attrs +$x = !empty($C['deny_attribute']) ? str_replace(array("\n", "\r", "\t", ' '), '', $C['deny_attribute']) : ''; +$x = array_flip((isset($x[0]) && $x[0] == '*') ? explode('-', $x) : explode(',', $x. (!empty($C['safe']) ? ',on*' : ''))); +if(isset($x['on*'])){ + unset($x['on*']); + $x += array('onblur'=>1, 'onchange'=>1, 'onclick'=>1, 'ondblclick'=>1, 'onfocus'=>1, 'onkeydown'=>1, 'onkeypress'=>1, 'onkeyup'=>1, 'onmousedown'=>1, 'onmousemove'=>1, 'onmouseout'=>1, 'onmouseover'=>1, 'onmouseup'=>1, 'onreset'=>1, 'onselect'=>1, 'onsubmit'=>1); +} +$C['deny_attribute'] = $x; +// config URL +$x = (isset($C['schemes'][2]) && strpos($C['schemes'], ':')) ? strtolower($C['schemes']) : 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https'; +$C['schemes'] = array(); +foreach(explode(';', str_replace(array(' ', "\t", "\r", "\n"), '', $x)) as $v){ + $x = $x2 = null; list($x, $x2) = explode(':', $v, 2); + if($x2){$C['schemes'][$x] = array_flip(explode(',', $x2));} +} +if(!isset($C['schemes']['*'])){$C['schemes']['*'] = array('file'=>1, 'http'=>1, 'https'=>1,);} +if(!empty($C['safe']) && empty($C['schemes']['style'])){$C['schemes']['style'] = array('!'=>1);} +$C['abs_url'] = isset($C['abs_url']) ? $C['abs_url'] : 0; +if(!isset($C['base_url']) or !preg_match('`^[a-zA-Z\d.+\-]+://[^/]+/(.+?/)?$`', $C['base_url'])){ + $C['base_url'] = $C['abs_url'] = 0; +} +// config rest +$C['and_mark'] = empty($C['and_mark']) ? 0 : 1; +$C['anti_link_spam'] = (isset($C['anti_link_spam']) && is_array($C['anti_link_spam']) && count($C['anti_link_spam']) == 2 && (empty($C['anti_link_spam'][0]) or hl_regex($C['anti_link_spam'][0])) && (empty($C['anti_link_spam'][1]) or hl_regex($C['anti_link_spam'][1]))) ? $C['anti_link_spam'] : 0; +$C['anti_mail_spam'] = isset($C['anti_mail_spam']) ? $C['anti_mail_spam'] : 0; +$C['balance'] = isset($C['balance']) ? (bool)$C['balance'] : 1; +$C['cdata'] = isset($C['cdata']) ? $C['cdata'] : (empty($C['safe']) ? 3 : 0); +$C['clean_ms_char'] = empty($C['clean_ms_char']) ? 0 : $C['clean_ms_char']; +$C['comment'] = isset($C['comment']) ? $C['comment'] : (empty($C['safe']) ? 3 : 0); +$C['css_expression'] = empty($C['css_expression']) ? 0 : 1; +$C['direct_list_nest'] = empty($C['direct_list_nest']) ? 0 : 1; +$C['hexdec_entity'] = isset($C['hexdec_entity']) ? $C['hexdec_entity'] : 1; +$C['hook'] = (!empty($C['hook']) && function_exists($C['hook'])) ? $C['hook'] : 0; +$C['hook_tag'] = (!empty($C['hook_tag']) && function_exists($C['hook_tag'])) ? $C['hook_tag'] : 0; +$C['keep_bad'] = isset($C['keep_bad']) ? $C['keep_bad'] : 6; +$C['lc_std_val'] = isset($C['lc_std_val']) ? (bool)$C['lc_std_val'] : 1; +$C['make_tag_strict'] = isset($C['make_tag_strict']) ? $C['make_tag_strict'] : 1; +$C['named_entity'] = isset($C['named_entity']) ? (bool)$C['named_entity'] : 1; +$C['no_deprecated_attr'] = isset($C['no_deprecated_attr']) ? $C['no_deprecated_attr'] : 1; +$C['parent'] = isset($C['parent'][0]) ? strtolower($C['parent']) : 'body'; +$C['show_setting'] = !empty($C['show_setting']) ? $C['show_setting'] : 0; +$C['style_pass'] = empty($C['style_pass']) ? 0 : 1; +$C['tidy'] = empty($C['tidy']) ? 0 : $C['tidy']; +$C['unique_ids'] = isset($C['unique_ids']) ? $C['unique_ids'] : 1; +$C['xml:lang'] = isset($C['xml:lang']) ? $C['xml:lang'] : 0; + +if(isset($GLOBALS['C'])){$reC = $GLOBALS['C'];} +$GLOBALS['C'] = $C; +$S = is_array($S) ? $S : hl_spec($S); +if(isset($GLOBALS['S'])){$reS = $GLOBALS['S'];} +$GLOBALS['S'] = $S; + +$t = preg_replace('`[\x00-\x08\x0b-\x0c\x0e-\x1f]`', '', $t); +if($C['clean_ms_char']){ + $x = array("\x7f"=>'', "\x80"=>'€', "\x81"=>'', "\x83"=>'ƒ', "\x85"=>'…', "\x86"=>'†', "\x87"=>'‡', "\x88"=>'ˆ', "\x89"=>'‰', "\x8a"=>'Š', "\x8b"=>'‹', "\x8c"=>'Œ', "\x8d"=>'', "\x8e"=>'Ž', "\x8f"=>'', "\x90"=>'', "\x95"=>'•', "\x96"=>'–', "\x97"=>'—', "\x98"=>'˜', "\x99"=>'™', "\x9a"=>'š', "\x9b"=>'›', "\x9c"=>'œ', "\x9d"=>'', "\x9e"=>'ž', "\x9f"=>'Ÿ'); + $x = $x + ($C['clean_ms_char'] == 1 ? array("\x82"=>'‚', "\x84"=>'„', "\x91"=>'‘', "\x92"=>'’', "\x93"=>'“', "\x94"=>'”') : array("\x82"=>'\'', "\x84"=>'"', "\x91"=>'\'', "\x92"=>'\'', "\x93"=>'"', "\x94"=>'"')); + $t = strtr($t, $x); +} +if($C['cdata'] or $C['comment']){$t = preg_replace_callback('`<!(?:(?:--.*?--)|(?:\[CDATA\[.*?\]\]))>`sm', 'hl_cmtcd', $t);} +$t = preg_replace_callback('`&([A-Za-z][A-Za-z0-9]{1,30}|#(?:[0-9]{1,8}|[Xx][0-9A-Fa-f]{1,7}));`', 'hl_ent', str_replace('&', '&', $t)); +if($C['unique_ids'] && !isset($GLOBALS['hl_Ids'])){$GLOBALS['hl_Ids'] = array();} +if($C['hook']){$t = $C['hook']($t, $C, $S);} +if($C['show_setting'] && preg_match('`^[a-z][a-z0-9_]*$`i', $C['show_setting'])){ + $GLOBALS[$C['show_setting']] = array('config'=>$C, 'spec'=>$S, 'time'=>microtime()); +} +// main +$t = preg_replace_callback('`<(?:(?:\s|$)|(?:[^>]*(?:>|$)))|>`m', 'hl_tag', $t); +$t = $C['balance'] ? hl_bal($t, $C['keep_bad'], $C['parent']) : $t; +$t = (($C['cdata'] or $C['comment']) && strpos($t, "\x01") !== false) ? str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05"), array('', '', '&', '<', '>'), $t) : $t; +$t = $C['tidy'] ? hl_tidy($t, $C['tidy'], $C['parent']) : $t; +unset($C, $e); +if(isset($reC)){$GLOBALS['C'] = $reC;} +if(isset($reS)){$GLOBALS['S'] = $reS;} +return $t; +// eof +} + +function hl_attrval($t, $p){ +// check attr val against $S +$o = 1; $l = strlen($t); +foreach($p as $k=>$v){ + switch($k){ + case 'maxlen':if($l > $v){$o = 0;} + break; case 'minlen': if($l < $v){$o = 0;} + break; case 'maxval': if((float)($t) > $v){$o = 0;} + break; case 'minval': if((float)($t) < $v){$o = 0;} + break; case 'match': if(!preg_match($v, $t)){$o = 0;} + break; case 'nomatch': if(preg_match($v, $t)){$o = 0;} + break; case 'oneof': + $m = 0; + foreach(explode('|', $v) as $n){if($t == $n){$m = 1; break;}} + $o = $m; + break; case 'noneof': + $m = 1; + foreach(explode('|', $v) as $n){if($t == $n){$m = 0; break;}} + $o = $m; + break; default: + break; + } + if(!$o){break;} +} +return ($o ? $t : (isset($p['default']) ? $p['default'] : 0)); +// eof +} + +function hl_bal($t, $do=1, $in='div'){ +// balance tags +// by content +$cB = array('blockquote'=>1, 'form'=>1, 'map'=>1, 'noscript'=>1); // Block +$cE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1); // Empty +$cF = array('button'=>1, 'del'=>1, 'div'=>1, 'dd'=>1, 'fieldset'=>1, 'iframe'=>1, 'ins'=>1, 'li'=>1, 'noscript'=>1, 'object'=>1, 'td'=>1, 'th'=>1); // Flow; later context-wise dynamic move of ins & del to $cI +$cI = array('a'=>1, 'abbr'=>1, 'acronym'=>1, 'address'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'caption'=>1, 'cite'=>1, 'code'=>1, 'dfn'=>1, 'dt'=>1, 'em'=>1, 'font'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'i'=>1, 'kbd'=>1, 'label'=>1, 'legend'=>1, 'p'=>1, 'pre'=>1, 'q'=>1, 'rb'=>1, 'rt'=>1, 's'=>1, 'samp'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'tt'=>1, 'u'=>1, 'var'=>1); // Inline +$cN = array('a'=>array('a'=>1), 'button'=>array('a'=>1, 'button'=>1, 'fieldset'=>1, 'form'=>1, 'iframe'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'fieldset'=>array('fieldset'=>1), 'form'=>array('form'=>1), 'label'=>array('label'=>1), 'noscript'=>array('script'=>1), 'pre'=>array('big'=>1, 'font'=>1, 'img'=>1, 'object'=>1, 'script'=>1, 'small'=>1, 'sub'=>1, 'sup'=>1), 'rb'=>array('ruby'=>1), 'rt'=>array('ruby'=>1)); // Illegal +$cN2 = array_keys($cN); +$cR = array('blockquote'=>1, 'dir'=>1, 'dl'=>1, 'form'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'optgroup'=>1, 'rbc'=>1, 'rtc'=>1, 'ruby'=>1, 'select'=>1, 'table'=>1, 'tbody'=>1, 'tfoot'=>1, 'thead'=>1, 'tr'=>1, 'ul'=>1); +$cS = array('colgroup'=>array('col'=>1), 'dir'=>array('li'=>1), 'dl'=>array('dd'=>1, 'dt'=>1), 'menu'=>array('li'=>1), 'ol'=>array('li'=>1), 'optgroup'=>array('option'=>1), 'option'=>array('#pcdata'=>1), 'rbc'=>array('rb'=>1), 'rp'=>array('#pcdata'=>1), 'rtc'=>array('rt'=>1), 'ruby'=>array('rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1), 'select'=>array('optgroup'=>1, 'option'=>1), 'script'=>array('#pcdata'=>1), 'table'=>array('caption'=>1, 'col'=>1, 'colgroup'=>1, 'tfoot'=>1, 'tbody'=>1, 'tr'=>1, 'thead'=>1), 'tbody'=>array('tr'=>1), 'tfoot'=>array('tr'=>1), 'textarea'=>array('#pcdata'=>1), 'thead'=>array('tr'=>1), 'tr'=>array('td'=>1, 'th'=>1), 'ul'=>array('li'=>1)); // Specific - immediate parent-child +if($GLOBALS['C']['direct_list_nest']){$cS['ol'] = $cS['ul'] += array('ol'=>1, 'ul'=>1);} +$cO = array('address'=>array('p'=>1), 'applet'=>array('param'=>1), 'blockquote'=>array('script'=>1), 'fieldset'=>array('legend'=>1, '#pcdata'=>1), 'form'=>array('script'=>1), 'map'=>array('area'=>1), 'object'=>array('param'=>1, 'embed'=>1)); // Other +$cT = array('colgroup'=>1, 'dd'=>1, 'dt'=>1, 'li'=>1, 'option'=>1, 'p'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1); // Omitable closing +// block/inline type; ins & del both type; #pcdata: text +$eB = array('address'=>1, 'blockquote'=>1, 'center'=>1, 'del'=>1, 'dir'=>1, 'dl'=>1, 'div'=>1, 'fieldset'=>1, 'form'=>1, 'ins'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'isindex'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'p'=>1, 'pre'=>1, 'table'=>1, 'ul'=>1); +$eI = array('#pcdata'=>1, 'a'=>1, 'abbr'=>1, 'acronym'=>1, 'applet'=>1, 'b'=>1, 'bdo'=>1, 'big'=>1, 'br'=>1, 'button'=>1, 'cite'=>1, 'code'=>1, 'del'=>1, 'dfn'=>1, 'em'=>1, 'embed'=>1, 'font'=>1, 'i'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'ins'=>1, 'kbd'=>1, 'label'=>1, 'map'=>1, 'object'=>1, 'q'=>1, 'ruby'=>1, 's'=>1, 'samp'=>1, 'select'=>1, 'script'=>1, 'small'=>1, 'span'=>1, 'strike'=>1, 'strong'=>1, 'sub'=>1, 'sup'=>1, 'textarea'=>1, 'tt'=>1, 'u'=>1, 'var'=>1); +$eN = array('a'=>1, 'big'=>1, 'button'=>1, 'fieldset'=>1, 'font'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'label'=>1, 'object'=>1, 'ruby'=>1, 'script'=>1, 'select'=>1, 'small'=>1, 'sub'=>1, 'sup'=>1, 'textarea'=>1); // Exclude from specific ele; $cN values +$eO = array('area'=>1, 'caption'=>1, 'col'=>1, 'colgroup'=>1, 'dd'=>1, 'dt'=>1, 'legend'=>1, 'li'=>1, 'optgroup'=>1, 'option'=>1, 'param'=>1, 'rb'=>1, 'rbc'=>1, 'rp'=>1, 'rt'=>1, 'rtc'=>1, 'script'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'thead'=>1, 'th'=>1, 'tr'=>1); // Missing in $eB & $eI +$eF = $eB + $eI; + +// $in sets allowed child +$in = ((isset($eF[$in]) && $in != '#pcdata') or isset($eO[$in])) ? $in : 'div'; +if(isset($cE[$in])){ + return (!$do ? '' : str_replace(array('<', '>'), array('<', '>'), $t)); +} +if(isset($cS[$in])){$inOk = $cS[$in];} +elseif(isset($cI[$in])){$inOk = $eI; $cI['del'] = 1; $cI['ins'] = 1;} +elseif(isset($cF[$in])){$inOk = $eF; unset($cI['del'], $cI['ins']);} +elseif(isset($cB[$in])){$inOk = $eB; unset($cI['del'], $cI['ins']);} +if(isset($cO[$in])){$inOk = $inOk + $cO[$in];} +if(isset($cN[$in])){$inOk = array_diff_assoc($inOk, $cN[$in]);} + +$t = explode('<', $t); +$ok = $q = array(); // $q seq list of open non-empty ele +ob_start(); + +for($i=-1, $ci=count($t); ++$i<$ci;){ + // allowed $ok in parent $p + if($ql = count($q)){ + $p = array_pop($q); + $q[] = $p; + if(isset($cS[$p])){$ok = $cS[$p];} + elseif(isset($cI[$p])){$ok = $eI; $cI['del'] = 1; $cI['ins'] = 1;} + elseif(isset($cF[$p])){$ok = $eF; unset($cI['del'], $cI['ins']);} + elseif(isset($cB[$p])){$ok = $eB; unset($cI['del'], $cI['ins']);} + if(isset($cO[$p])){$ok = $ok + $cO[$p];} + if(isset($cN[$p])){$ok = array_diff_assoc($ok, $cN[$p]);} + }else{$ok = $inOk; unset($cI['del'], $cI['ins']);} + // bad tags, & ele content + if(isset($e) && ($do == 1 or (isset($ok['#pcdata']) && ($do == 3 or $do == 5)))){ + echo '<', $s, $e, $a, '>'; + } + if(isset($x[0])){ + if($do < 3 or isset($ok['#pcdata'])){echo $x;} + elseif(strpos($x, "\x02\x04")){ + foreach(preg_split('`(\x01\x02[^\x01\x02]+\x02\x01)`', $x, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) as $v){ + echo (substr($v, 0, 2) == "\x01\x02" ? $v : ($do > 4 ? preg_replace('`\S`', '', $v) : '')); + } + }elseif($do > 4){echo preg_replace('`\S`', '', $x);} + } + // get markup + if(!preg_match('`^(/?)([a-zA-Z1-6]+)([^>]*)>(.*)`sm', $t[$i], $r)){$x = $t[$i]; continue;} + $s = null; $e = null; $a = null; $x = null; list($all, $s, $e, $a, $x) = $r; + // close tag + if($s){ + if(isset($cE[$e]) or !in_array($e, $q)){continue;} // Empty/unopen + if($p == $e){array_pop($q); echo '</', $e, '>'; unset($e); continue;} // Last open + $add = ''; // Nesting - close open tags that need to be + for($j=-1, $cj=count($q); ++$j<$cj;){ + if(($d = array_pop($q)) == $e){break;} + else{$add .= "</{$d}>";} + } + echo $add, '</', $e, '>'; unset($e); continue; + } + // open tag + // $cB ele needs $eB ele as child + if(isset($cB[$e]) && strlen(trim($x))){ + $t[$i] = "{$e}{$a}>"; + array_splice($t, $i+1, 0, 'div>'. $x); unset($e, $x); ++$ci; --$i; continue; + } + if((($ql && isset($cB[$p])) or (isset($cB[$in]) && !$ql)) && !isset($eB[$e]) && !isset($ok[$e])){ + array_splice($t, $i, 0, 'div>'); unset($e, $x); ++$ci; --$i; continue; + } + // if no open ele, $in = parent; mostly immediate parent-child relation should hold + if(!$ql or !isset($eN[$e]) or !array_intersect($q, $cN2)){ + if(!isset($ok[$e])){ + if($ql && isset($cT[$p])){echo '</', array_pop($q), '>'; unset($e, $x); --$i;} + continue; + } + if(!isset($cE[$e])){$q[] = $e;} + echo '<', $e, $a, '>'; unset($e); continue; + } + // specific parent-child + if(isset($cS[$p][$e])){ + if(!isset($cE[$e])){$q[] = $e;} + echo '<', $e, $a, '>'; unset($e); continue; + } + // nesting + $add = ''; + $q2 = array(); + for($k=-1, $kc=count($q); ++$k<$kc;){ + $d = $q[$k]; + $ok2 = array(); + if(isset($cS[$d])){$q2[] = $d; continue;} + $ok2 = isset($cI[$d]) ? $eI : $eF; + if(isset($cO[$d])){$ok2 = $ok2 + $cO[$d];} + if(isset($cN[$d])){$ok2 = array_diff_assoc($ok2, $cN[$d]);} + if(!isset($ok2[$e])){ + if(!$k && !isset($inOk[$e])){continue 2;} + $add = "</{$d}>"; + for(;++$k<$kc;){$add = "</{$q[$k]}>{$add}";} + break; + } + else{$q2[] = $d;} + } + $q = $q2; + if(!isset($cE[$e])){$q[] = $e;} + echo $add, '<', $e, $a, '>'; unset($e); continue; +} + +// end +if($ql = count($q)){ + $p = array_pop($q); + $q[] = $p; + if(isset($cS[$p])){$ok = $cS[$p];} + elseif(isset($cI[$p])){$ok = $eI; $cI['del'] = 1; $cI['ins'] = 1;} + elseif(isset($cF[$p])){$ok = $eF; unset($cI['del'], $cI['ins']);} + elseif(isset($cB[$p])){$ok = $eB; unset($cI['del'], $cI['ins']);} + if(isset($cO[$p])){$ok = $ok + $cO[$p];} + if(isset($cN[$p])){$ok = array_diff_assoc($ok, $cN[$p]);} +}else{$ok = $inOk; unset($cI['del'], $cI['ins']);} +if(isset($e) && ($do == 1 or (isset($ok['#pcdata']) && ($do == 3 or $do == 5)))){ + echo '<', $s, $e, $a, '>'; +} +if(isset($x[0])){ + if(strlen(trim($x)) && (($ql && isset($cB[$p])) or (isset($cB[$in]) && !$ql))){ + echo '<div>', $x, '</div>'; + } + elseif($do < 3 or isset($ok['#pcdata'])){echo $x;} + elseif(strpos($x, "\x02\x04")){ + foreach(preg_split('`(\x01\x02[^\x01\x02]+\x02\x01)`', $x, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) as $v){ + echo (substr($v, 0, 2) == "\x01\x02" ? $v : ($do > 4 ? preg_replace('`\S`', '', $v) : '')); + } + }elseif($do > 4){echo preg_replace('`\S`', '', $x);} +} +while(!empty($q) && ($e = array_pop($q))){echo '</', $e, '>';} +$o = ob_get_contents(); +ob_end_clean(); +return $o; +// eof +} + +function hl_cmtcd($t){ +// comment/CDATA sec handler +$t = $t[0]; +global $C; +if(!($v = $C[$n = $t[3] == '-' ? 'comment' : 'cdata'])){return $t;} +if($v == 1){return '';} +if($n == 'comment'){ + if(substr(($t = preg_replace('`--+`', '-', substr($t, 4, -3))), -1) != ' '){$t .= ' ';} +} +else{$t = substr($t, 1, -1);} +$t = $v == 2 ? str_replace(array('&', '<', '>'), array('&', '<', '>'), $t) : $t; +return str_replace(array('&', '<', '>'), array("\x03", "\x04", "\x05"), ($n == 'comment' ? "\x01\x02\x04!--$t--\x05\x02\x01" : "\x01\x01\x04$t\x05\x01\x01")); +// eof +} + +function hl_ent($t){ +// entitity handler +global $C; +$t = $t[1]; +static $U = array('quot'=>1,'amp'=>1,'lt'=>1,'gt'=>1); +static $N = array('fnof'=>'402', 'Alpha'=>'913', 'Beta'=>'914', 'Gamma'=>'915', 'Delta'=>'916', 'Epsilon'=>'917', 'Zeta'=>'918', 'Eta'=>'919', 'Theta'=>'920', 'Iota'=>'921', 'Kappa'=>'922', 'Lambda'=>'923', 'Mu'=>'924', 'Nu'=>'925', 'Xi'=>'926', 'Omicron'=>'927', 'Pi'=>'928', 'Rho'=>'929', 'Sigma'=>'931', 'Tau'=>'932', 'Upsilon'=>'933', 'Phi'=>'934', 'Chi'=>'935', 'Psi'=>'936', 'Omega'=>'937', 'alpha'=>'945', 'beta'=>'946', 'gamma'=>'947', 'delta'=>'948', 'epsilon'=>'949', 'zeta'=>'950', 'eta'=>'951', 'theta'=>'952', 'iota'=>'953', 'kappa'=>'954', 'lambda'=>'955', 'mu'=>'956', 'nu'=>'957', 'xi'=>'958', 'omicron'=>'959', 'pi'=>'960', 'rho'=>'961', 'sigmaf'=>'962', 'sigma'=>'963', 'tau'=>'964', 'upsilon'=>'965', 'phi'=>'966', 'chi'=>'967', 'psi'=>'968', 'omega'=>'969', 'thetasym'=>'977', 'upsih'=>'978', 'piv'=>'982', 'bull'=>'8226', 'hellip'=>'8230', 'prime'=>'8242', 'Prime'=>'8243', 'oline'=>'8254', 'frasl'=>'8260', 'weierp'=>'8472', 'image'=>'8465', 'real'=>'8476', 'trade'=>'8482', 'alefsym'=>'8501', 'larr'=>'8592', 'uarr'=>'8593', 'rarr'=>'8594', 'darr'=>'8595', 'harr'=>'8596', 'crarr'=>'8629', 'lArr'=>'8656', 'uArr'=>'8657', 'rArr'=>'8658', 'dArr'=>'8659', 'hArr'=>'8660', 'forall'=>'8704', 'part'=>'8706', 'exist'=>'8707', 'empty'=>'8709', 'nabla'=>'8711', 'isin'=>'8712', 'notin'=>'8713', 'ni'=>'8715', 'prod'=>'8719', 'sum'=>'8721', 'minus'=>'8722', 'lowast'=>'8727', 'radic'=>'8730', 'prop'=>'8733', 'infin'=>'8734', 'ang'=>'8736', 'and'=>'8743', 'or'=>'8744', 'cap'=>'8745', 'cup'=>'8746', 'int'=>'8747', 'there4'=>'8756', 'sim'=>'8764', 'cong'=>'8773', 'asymp'=>'8776', 'ne'=>'8800', 'equiv'=>'8801', 'le'=>'8804', 'ge'=>'8805', 'sub'=>'8834', 'sup'=>'8835', 'nsub'=>'8836', 'sube'=>'8838', 'supe'=>'8839', 'oplus'=>'8853', 'otimes'=>'8855', 'perp'=>'8869', 'sdot'=>'8901', 'lceil'=>'8968', 'rceil'=>'8969', 'lfloor'=>'8970', 'rfloor'=>'8971', 'lang'=>'9001', 'rang'=>'9002', 'loz'=>'9674', 'spades'=>'9824', 'clubs'=>'9827', 'hearts'=>'9829', 'diams'=>'9830', 'apos'=>'39', 'OElig'=>'338', 'oelig'=>'339', 'Scaron'=>'352', 'scaron'=>'353', 'Yuml'=>'376', 'circ'=>'710', 'tilde'=>'732', 'ensp'=>'8194', 'emsp'=>'8195', 'thinsp'=>'8201', 'zwnj'=>'8204', 'zwj'=>'8205', 'lrm'=>'8206', 'rlm'=>'8207', 'ndash'=>'8211', 'mdash'=>'8212', 'lsquo'=>'8216', 'rsquo'=>'8217', 'sbquo'=>'8218', 'ldquo'=>'8220', 'rdquo'=>'8221', 'bdquo'=>'8222', 'dagger'=>'8224', 'Dagger'=>'8225', 'permil'=>'8240', 'lsaquo'=>'8249', 'rsaquo'=>'8250', 'euro'=>'8364', 'nbsp'=>'160', 'iexcl'=>'161', 'cent'=>'162', 'pound'=>'163', 'curren'=>'164', 'yen'=>'165', 'brvbar'=>'166', 'sect'=>'167', 'uml'=>'168', 'copy'=>'169', 'ordf'=>'170', 'laquo'=>'171', 'not'=>'172', 'shy'=>'173', 'reg'=>'174', 'macr'=>'175', 'deg'=>'176', 'plusmn'=>'177', 'sup2'=>'178', 'sup3'=>'179', 'acute'=>'180', 'micro'=>'181', 'para'=>'182', 'middot'=>'183', 'cedil'=>'184', 'sup1'=>'185', 'ordm'=>'186', 'raquo'=>'187', 'frac14'=>'188', 'frac12'=>'189', 'frac34'=>'190', 'iquest'=>'191', 'Agrave'=>'192', 'Aacute'=>'193', 'Acirc'=>'194', 'Atilde'=>'195', 'Auml'=>'196', 'Aring'=>'197', 'AElig'=>'198', 'Ccedil'=>'199', 'Egrave'=>'200', 'Eacute'=>'201', 'Ecirc'=>'202', 'Euml'=>'203', 'Igrave'=>'204', 'Iacute'=>'205', 'Icirc'=>'206', 'Iuml'=>'207', 'ETH'=>'208', 'Ntilde'=>'209', 'Ograve'=>'210', 'Oacute'=>'211', 'Ocirc'=>'212', 'Otilde'=>'213', 'Ouml'=>'214', 'times'=>'215', 'Oslash'=>'216', 'Ugrave'=>'217', 'Uacute'=>'218', 'Ucirc'=>'219', 'Uuml'=>'220', 'Yacute'=>'221', 'THORN'=>'222', 'szlig'=>'223', 'agrave'=>'224', 'aacute'=>'225', 'acirc'=>'226', 'atilde'=>'227', 'auml'=>'228', 'aring'=>'229', 'aelig'=>'230', 'ccedil'=>'231', 'egrave'=>'232', 'eacute'=>'233', 'ecirc'=>'234', 'euml'=>'235', 'igrave'=>'236', 'iacute'=>'237', 'icirc'=>'238', 'iuml'=>'239', 'eth'=>'240', 'ntilde'=>'241', 'ograve'=>'242', 'oacute'=>'243', 'ocirc'=>'244', 'otilde'=>'245', 'ouml'=>'246', 'divide'=>'247', 'oslash'=>'248', 'ugrave'=>'249', 'uacute'=>'250', 'ucirc'=>'251', 'uuml'=>'252', 'yacute'=>'253', 'thorn'=>'254', 'yuml'=>'255'); +if($t[0] != '#'){ + return ($C['and_mark'] ? "\x06" : '&'). (isset($U[$t]) ? $t : (isset($N[$t]) ? (!$C['named_entity'] ? '#'. ($C['hexdec_entity'] > 1 ? 'x'. dechex($N[$t]) : $N[$t]) : $t) : 'amp;'. $t)). ';'; +} +if(($n = ctype_digit($t = substr($t, 1)) ? intval($t) : hexdec(substr($t, 1))) < 9 or ($n > 13 && $n < 32) or $n == 11 or $n == 12 or ($n > 126 && $n < 160 && $n != 133) or ($n > 55295 && ($n < 57344 or ($n > 64975 && $n < 64992) or $n == 65534 or $n == 65535 or $n > 1114111))){ + return ($C['and_mark'] ? "\x06" : '&'). "amp;#{$t};"; +} +return ($C['and_mark'] ? "\x06" : '&'). '#'. (((ctype_digit($t) && $C['hexdec_entity'] < 2) or !$C['hexdec_entity']) ? $n : 'x'. dechex($n)). ';'; +// eof +} + +function hl_prot($p, $c=null){ +// check URL scheme +global $C; +$b = $a = ''; +if($c == null){$c = 'style'; $b = $p[1]; $a = $p[3]; $p = trim($p[2]);} +$c = isset($C['schemes'][$c]) ? $C['schemes'][$c] : $C['schemes']['*']; +static $d = 'denied:'; +if(isset($c['!']) && substr($p, 0, 7) != $d){$p = "$d$p";} +if(isset($c['*']) or !strcspn($p, '#?;') or (substr($p, 0, 7) == $d)){return "{$b}{$p}{$a}";} // All ok, frag, query, param +if(preg_match('`^([a-z\d\-+.&#; ]+?)(:|&#(58|x3a);|%3a|\\\\0{0,4}3a).`i', $p, $m) && !isset($c[strtolower($m[1])])){ // Denied prot + return "{$b}{$d}{$p}{$a}"; +} +if($C['abs_url']){ + if($C['abs_url'] == -1 && strpos($p, $C['base_url']) === 0){ // Make url rel + $p = substr($p, strlen($C['base_url'])); + }elseif(empty($m[1])){ // Make URL abs + if(substr($p, 0, 2) == '//'){$p = substr($C['base_url'], 0, strpos($C['base_url'], ':')+1). $p;} + elseif($p[0] == '/'){$p = preg_replace('`(^.+?://[^/]+)(.*)`', '$1', $C['base_url']). $p;} + elseif(strcspn($p, './')){$p = $C['base_url']. $p;} + else{ + preg_match('`^([a-zA-Z\d\-+.]+://[^/]+)(.*)`', $C['base_url'], $m); + $p = preg_replace('`(?<=/)\./`', '', $m[2]. $p); + while(preg_match('`(?<=/)([^/]{3,}|[^/.]+?|\.[^/.]|[^/.]\.)/\.\./`', $p)){ + $p = preg_replace('`(?<=/)([^/]{3,}|[^/.]+?|\.[^/.]|[^/.]\.)/\.\./`', '', $p); + } + $p = $m[1]. $p; + } + } +} +return "{$b}{$p}{$a}"; +// eof +} + +function hl_regex($p){ +// ?regex +if(empty($p)){return 0;} +if($t = ini_get('track_errors')){$o = isset($php_errormsg) ? $php_errormsg : null;} +else{ini_set('track_errors', 1);} +unset($php_errormsg); +if(($d = ini_get('display_errors'))){ini_set('display_errors', 0);} +preg_match($p, ''); +if($d){ini_set('display_errors', 1);} +$r = isset($php_errormsg) ? 0 : 1; +if($t){$php_errormsg = isset($o) ? $o : null;} +else{ini_set('track_errors', 0);} +return $r; +// eof +} + +function hl_spec($t){ +// final $spec +$s = array(); +$t = str_replace(array("\t", "\r", "\n", ' '), '', preg_replace('/"(?>(`.|[^"])*)"/sme', 'substr(str_replace(array(";", "|", "~", " ", ",", "/", "(", ")", \'`"\'), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\""), "$0"), 1, -1)', trim($t))); +for($i = count(($t = explode(';', $t))); --$i>=0;){ + $w = $t[$i]; + if(empty($w) or ($e = strpos($w, '=')) === false or !strlen(($a = substr($w, $e+1)))){continue;} + $y = $n = array(); + foreach(explode(',', $a) as $v){ + if(!preg_match('`^([a-z:\-\*]+)(?:\((.*?)\))?`i', $v, $m)){continue;} + if(($x = strtolower($m[1])) == '-*'){$n['*'] = 1; continue;} + if($x[0] == '-'){$n[substr($x, 1)] = 1; continue;} + if(!isset($m[2])){$y[$x] = 1; continue;} + foreach(explode('/', $m[2]) as $m){ + if(empty($m) or ($p = strpos($m, '=')) == 0 or $p < 5){$y[$x] = 1; continue;} + $y[$x][strtolower(substr($m, 0, $p))] = str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08"), array(";", "|", "~", " ", ",", "/", "(", ")"), substr($m, $p+1)); + } + if(isset($y[$x]['match']) && !hl_regex($y[$x]['match'])){unset($y[$x]['match']);} + if(isset($y[$x]['nomatch']) && !hl_regex($y[$x]['nomatch'])){unset($y[$x]['nomatch']);} + } + if(!count($y) && !count($n)){continue;} + foreach(explode(',', substr($w, 0, $e)) as $v){ + if(!strlen(($v = strtolower($v)))){continue;} + if(count($y)){$s[$v] = $y;} + if(count($n)){$s[$v]['n'] = $n;} + } +} +return $s; +// eof +} + +function hl_tag($t){ +// tag/attribute handler +global $C; +$t = $t[0]; +// invalid < > +if($t == '< '){return '< ';} +if($t == '>'){return '>';} +if(!preg_match('`^<(/?)([a-zA-Z][a-zA-Z1-6]*)([^>]*?)\s?>$`m', $t, $m)){ + return str_replace(array('<', '>'), array('<', '>'), $t); +}elseif(!isset($C['elements'][($e = strtolower($m[2]))])){ + return (($C['keep_bad']%2) ? str_replace(array('<', '>'), array('<', '>'), $t) : ''); +} +// attr string +$a = str_replace(array("\n", "\r", "\t"), ' ', trim($m[3])); +// tag transform +static $eD = array('applet'=>1, 'center'=>1, 'dir'=>1, 'embed'=>1, 'font'=>1, 'isindex'=>1, 'menu'=>1, 's'=>1, 'strike'=>1, 'u'=>1); // Deprecated +if($C['make_tag_strict'] && isset($eD[$e])){ + $trt = hl_tag2($e, $a, $C['make_tag_strict']); + if(!$e){return (($C['keep_bad']%2) ? str_replace(array('<', '>'), array('<', '>'), $t) : '');} +} +// close tag +static $eE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1); // Empty ele +if(!empty($m[1])){ + return (!isset($eE[$e]) ? "</$e>" : (($C['keep_bad'])%2 ? str_replace(array('<', '>'), array('<', '>'), $t) : '')); +} + +// open tag & attr +static $aN = array('abbr'=>array('td'=>1, 'th'=>1), 'accept-charset'=>array('form'=>1), 'accept'=>array('form'=>1, 'input'=>1), 'accesskey'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'legend'=>1, 'textarea'=>1), 'action'=>array('form'=>1), 'align'=>array('caption'=>1, 'embed'=>1, 'applet'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'object'=>1, 'legend'=>1, 'table'=>1, 'hr'=>1, 'div'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'p'=>1, 'col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'alt'=>array('applet'=>1, 'area'=>1, 'img'=>1, 'input'=>1), 'archive'=>array('applet'=>1, 'object'=>1), 'axis'=>array('td'=>1, 'th'=>1), 'bgcolor'=>array('embed'=>1, 'table'=>1, 'tr'=>1, 'td'=>1, 'th'=>1), 'border'=>array('table'=>1, 'img'=>1, 'object'=>1), 'bordercolor'=>array('table'=>1, 'td'=>1, 'tr'=>1), 'cellpadding'=>array('table'=>1), 'cellspacing'=>array('table'=>1), 'char'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'charoff'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'charset'=>array('a'=>1, 'script'=>1), 'checked'=>array('input'=>1), 'cite'=>array('blockquote'=>1, 'q'=>1, 'del'=>1, 'ins'=>1), 'classid'=>array('object'=>1), 'clear'=>array('br'=>1), 'code'=>array('applet'=>1), 'codebase'=>array('object'=>1, 'applet'=>1), 'codetype'=>array('object'=>1), 'color'=>array('font'=>1), 'cols'=>array('textarea'=>1), 'colspan'=>array('td'=>1, 'th'=>1), 'compact'=>array('dir'=>1, 'dl'=>1, 'menu'=>1, 'ol'=>1, 'ul'=>1), 'coords'=>array('area'=>1, 'a'=>1), 'data'=>array('object'=>1), 'datetime'=>array('del'=>1, 'ins'=>1), 'declare'=>array('object'=>1), 'defer'=>array('script'=>1), 'dir'=>array('bdo'=>1), 'disabled'=>array('button'=>1, 'input'=>1, 'optgroup'=>1, 'option'=>1, 'select'=>1, 'textarea'=>1), 'enctype'=>array('form'=>1), 'face'=>array('font'=>1), 'flashvars'=>array('embed'=>1), 'for'=>array('label'=>1), 'frame'=>array('table'=>1), 'frameborder'=>array('iframe'=>1), 'headers'=>array('td'=>1, 'th'=>1), 'height'=>array('embed'=>1, 'iframe'=>1, 'td'=>1, 'th'=>1, 'img'=>1, 'object'=>1, 'applet'=>1), 'href'=>array('a'=>1, 'area'=>1), 'hreflang'=>array('a'=>1), 'hspace'=>array('applet'=>1, 'img'=>1, 'object'=>1), 'ismap'=>array('img'=>1, 'input'=>1), 'label'=>array('option'=>1, 'optgroup'=>1), 'language'=>array('script'=>1), 'longdesc'=>array('img'=>1, 'iframe'=>1), 'marginheight'=>array('iframe'=>1), 'marginwidth'=>array('iframe'=>1), 'maxlength'=>array('input'=>1), 'method'=>array('form'=>1), 'model'=>array('embed'=>1), 'multiple'=>array('select'=>1), 'name'=>array('button'=>1, 'embed'=>1, 'textarea'=>1, 'applet'=>1, 'select'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'a'=>1, 'input'=>1, 'object'=>1, 'map'=>1, 'param'=>1), 'nohref'=>array('area'=>1), 'noshade'=>array('hr'=>1), 'nowrap'=>array('td'=>1, 'th'=>1), 'object'=>array('applet'=>1), 'onblur'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'onchange'=>array('input'=>1, 'select'=>1, 'textarea'=>1), 'onfocus'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'label'=>1, 'select'=>1, 'textarea'=>1), 'onreset'=>array('form'=>1), 'onselect'=>array('input'=>1, 'textarea'=>1), 'onsubmit'=>array('form'=>1), 'pluginspage'=>array('embed'=>1), 'pluginurl'=>array('embed'=>1), 'prompt'=>array('isindex'=>1), 'readonly'=>array('textarea'=>1, 'input'=>1), 'rel'=>array('a'=>1), 'rev'=>array('a'=>1), 'rows'=>array('textarea'=>1), 'rowspan'=>array('td'=>1, 'th'=>1), 'rules'=>array('table'=>1), 'scope'=>array('td'=>1, 'th'=>1), 'scrolling'=>array('iframe'=>1), 'selected'=>array('option'=>1), 'shape'=>array('area'=>1, 'a'=>1), 'size'=>array('hr'=>1, 'font'=>1, 'input'=>1, 'select'=>1), 'span'=>array('col'=>1, 'colgroup'=>1), 'src'=>array('embed'=>1, 'script'=>1, 'input'=>1, 'iframe'=>1, 'img'=>1), 'standby'=>array('object'=>1), 'start'=>array('ol'=>1), 'summary'=>array('table'=>1), 'tabindex'=>array('a'=>1, 'area'=>1, 'button'=>1, 'input'=>1, 'object'=>1, 'select'=>1, 'textarea'=>1), 'target'=>array('a'=>1, 'area'=>1, 'form'=>1), 'type'=>array('a'=>1, 'embed'=>1, 'object'=>1, 'param'=>1, 'script'=>1, 'input'=>1, 'li'=>1, 'ol'=>1, 'ul'=>1, 'button'=>1), 'usemap'=>array('img'=>1, 'input'=>1, 'object'=>1), 'valign'=>array('col'=>1, 'colgroup'=>1, 'tbody'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1), 'value'=>array('input'=>1, 'option'=>1, 'param'=>1, 'button'=>1, 'li'=>1), 'valuetype'=>array('param'=>1), 'vspace'=>array('applet'=>1, 'img'=>1, 'object'=>1), 'width'=>array('embed'=>1, 'hr'=>1, 'iframe'=>1, 'img'=>1, 'object'=>1, 'table'=>1, 'td'=>1, 'th'=>1, 'applet'=>1, 'col'=>1, 'colgroup'=>1, 'pre'=>1), 'wmode'=>array('embed'=>1), 'xml:space'=>array('pre'=>1, 'script'=>1, 'style'=>1)); // Ele-specific +static $aNE = array('checked'=>1, 'compact'=>1, 'declare'=>1, 'defer'=>1, 'disabled'=>1, 'ismap'=>1, 'multiple'=>1, 'nohref'=>1, 'noresize'=>1, 'noshade'=>1, 'nowrap'=>1, 'readonly'=>1, 'selected'=>1); // Empty +static $aNP = array('action'=>1, 'cite'=>1, 'classid'=>1, 'codebase'=>1, 'data'=>1, 'href'=>1, 'longdesc'=>1, 'model'=>1, 'pluginspage'=>1, 'pluginurl'=>1, 'usemap'=>1); // Need scheme check; excludes style, on* & src +static $aNU = array('class'=>array('param'=>1, 'script'=>1), 'dir'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'id'=>array('script'=>1), 'lang'=>array('applet'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'xml:lang'=>array('applet'=>1, 'br'=>1, 'iframe'=>1, 'param'=>1, 'script'=>1), 'onclick'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'ondblclick'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeydown'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeypress'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onkeyup'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmousedown'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmousemove'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseout'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseover'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'onmouseup'=>array('applet'=>1, 'bdo'=>1, 'br'=>1, 'font'=>1, 'iframe'=>1, 'isindex'=>1, 'param'=>1, 'script'=>1), 'style'=>array('param'=>1, 'script'=>1), 'title'=>array('param'=>1, 'script'=>1)); // Univ & exceptions + +if($C['lc_std_val']){ + // predef attr vals for $eAL & $aNE ele + static $aNL = array('all'=>1, 'baseline'=>1, 'bottom'=>1, 'button'=>1, 'center'=>1, 'char'=>1, 'checkbox'=>1, 'circle'=>1, 'col'=>1, 'colgroup'=>1, 'cols'=>1, 'data'=>1, 'default'=>1, 'file'=>1, 'get'=>1, 'groups'=>1, 'hidden'=>1, 'image'=>1, 'justify'=>1, 'left'=>1, 'ltr'=>1, 'middle'=>1, 'none'=>1, 'object'=>1, 'password'=>1, 'poly'=>1, 'post'=>1, 'preserve'=>1, 'radio'=>1, 'rect'=>1, 'ref'=>1, 'reset'=>1, 'right'=>1, 'row'=>1, 'rowgroup'=>1, 'rows'=>1, 'rtl'=>1, 'submit'=>1, 'text'=>1, 'top'=>1); + static $eAL = array('a'=>1, 'area'=>1, 'bdo'=>1, 'button'=>1, 'col'=>1, 'form'=>1, 'img'=>1, 'input'=>1, 'object'=>1, 'optgroup'=>1, 'option'=>1, 'param'=>1, 'script'=>1, 'select'=>1, 'table'=>1, 'td'=>1, 'tfoot'=>1, 'th'=>1, 'thead'=>1, 'tr'=>1, 'xml:space'=>1); + $lcase = isset($eAL[$e]) ? 1 : 0; +} + +$depTr = 0; +if($C['no_deprecated_attr']){ + // dep attr:applicable ele + static $aND = array('align'=>array('caption'=>1, 'div'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'img'=>1, 'input'=>1, 'legend'=>1, 'object'=>1, 'p'=>1, 'table'=>1), 'bgcolor'=>array('table'=>1, 'td'=>1, 'th'=>1, 'tr'=>1), 'border'=>array('img'=>1, 'object'=>1), 'bordercolor'=>array('table'=>1, 'td'=>1, 'tr'=>1), 'clear'=>array('br'=>1), 'compact'=>array('dl'=>1, 'ol'=>1, 'ul'=>1), 'height'=>array('td'=>1, 'th'=>1), 'hspace'=>array('img'=>1, 'object'=>1), 'language'=>array('script'=>1), 'name'=>array('a'=>1, 'form'=>1, 'iframe'=>1, 'img'=>1, 'map'=>1), 'noshade'=>array('hr'=>1), 'nowrap'=>array('td'=>1, 'th'=>1), 'size'=>array('hr'=>1), 'start'=>array('ol'=>1), 'type'=>array('li'=>1, 'ol'=>1, 'ul'=>1), 'value'=>array('li'=>1), 'vspace'=>array('img'=>1, 'object'=>1), 'width'=>array('hr'=>1, 'pre'=>1, 'td'=>1, 'th'=>1)); + static $eAD = array('a'=>1, 'br'=>1, 'caption'=>1, 'div'=>1, 'dl'=>1, 'form'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'hr'=>1, 'iframe'=>1, 'img'=>1, 'input'=>1, 'legend'=>1, 'li'=>1, 'map'=>1, 'object'=>1, 'ol'=>1, 'p'=>1, 'pre'=>1, 'script'=>1, 'table'=>1, 'td'=>1, 'th'=>1, 'tr'=>1, 'ul'=>1); + $depTr = isset($eAD[$e]) ? 1 : 0; +} + +// attr name-vals +if(strpos($a, "\x01") !== false){$a = preg_replace('`\x01[^\x01]*\x01`', '', $a);} // No comment/CDATA sec +$mode = 0; $a = trim($a, ' /'); $aA = array(); +while(strlen($a)){ + $w = 0; + switch($mode){ + case 0: // Name + if(preg_match('`^[a-zA-Z][\-a-zA-Z:]+`', $a, $m)){ + $nm = strtolower($m[0]); + $w = $mode = 1; $a = ltrim(substr_replace($a, '', 0, strlen($m[0]))); + } + break; case 1: + if($a[0] == '='){ // = + $w = 1; $mode = 2; $a = ltrim($a, '= '); + }else{ // No val + $w = 1; $mode = 0; $a = ltrim($a); + $aA[$nm] = ''; + } + break; case 2: // Val + if(preg_match('`^"[^"]*"`', $a, $m) or preg_match("`^'[^']*'`", $a, $m) or preg_match("`^\s*[^\s\"']+`", $a, $m)){ + $m = $m[0]; $w = 1; $mode = 0; $a = ltrim(substr_replace($a, '', 0, strlen($m))); + $aA[$nm] = trim(($m[0] == '"' or $m[0] == '\'') ? substr($m, 1, -1) : $m); + } + break; + } + if($w == 0){ // Parse errs, deal with space, " & ' + $a = preg_replace('`^(?:"[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*`', '', $a); + $mode = 0; + } +} +if($mode == 1){$aA[$nm] = '';} + +// clean attrs +global $S; +$rl = isset($S[$e]) ? $S[$e] : array(); +$a = array(); $nfr = 0; +foreach($aA as $k=>$v){ + if(((isset($C['deny_attribute']['*']) ? isset($C['deny_attribute'][$k]) : !isset($C['deny_attribute'][$k])) or isset($rl[$k])) && ((!isset($rl['n'][$k]) && !isset($rl['n']['*'])) or isset($rl[$k])) && (isset($aN[$k][$e]) or (isset($aNU[$k]) && !isset($aNU[$k][$e])))){ + if(isset($aNE[$k])){$v = $k;} + elseif(!empty($lcase) && (($e != 'button' or $e != 'input') or $k == 'type')){ // Rather loose but ?not cause issues + $v = (isset($aNL[($v2 = strtolower($v))])) ? $v2 : $v; + } + if($k == 'style' && !$C['style_pass']){ + if(false !== strpos($v, '&#')){ + static $sC = array(' '=>' ', ' '=>' ', 'E'=>'e', 'E'=>'e', 'e'=>'e', 'e'=>'e', 'X'=>'x', 'X'=>'x', 'x'=>'x', 'x'=>'x', 'P'=>'p', 'P'=>'p', 'p'=>'p', 'p'=>'p', 'S'=>'s', 'S'=>'s', 's'=>'s', 's'=>'s', 'I'=>'i', 'I'=>'i', 'i'=>'i', 'i'=>'i', 'O'=>'o', 'O'=>'o', 'o'=>'o', 'o'=>'o', 'N'=>'n', 'N'=>'n', 'n'=>'n', 'n'=>'n', 'U'=>'u', 'U'=>'u', 'u'=>'u', 'u'=>'u', 'R'=>'r', 'R'=>'r', 'r'=>'r', 'r'=>'r', 'L'=>'l', 'L'=>'l', 'l'=>'l', 'l'=>'l', '('=>'(', '('=>'(', ')'=>')', ')'=>')', ' '=>':', ' '=>':', '"'=>'"', '"'=>'"', '''=>"'", '''=>"'", '/'=>'/', '/'=>'/', '*'=>'*', '*'=>'*', '\'=>'\\', '\'=>'\\'); + $v = strtr($v, $sC); + } + $v = preg_replace_callback('`(url(?:\()(?: )*(?:\'|"|&(?:quot|apos);)?)(.+?)((?:\'|"|&(?:quot|apos);)?(?: )*(?:\)))`iS', 'hl_prot', $v); + $v = !$C['css_expression'] ? preg_replace('`expression`i', ' ', preg_replace('`\\\\\S|(/|(%2f))(\*|(%2a))`i', ' ', $v)) : $v; + }elseif(isset($aNP[$k]) or strpos($k, 'src') !== false or $k[0] == 'o'){ + $v = str_replace("\xad", ' ', (strpos($v, '&') !== false ? str_replace(array('­', '­', '­'), ' ', $v) : $v)); + $v = hl_prot($v, $k); + if($k == 'href'){ // X-spam + if($C['anti_mail_spam'] && strpos($v, 'mailto:') === 0){ + $v = str_replace('@', htmlspecialchars($C['anti_mail_spam']), $v); + }elseif($C['anti_link_spam']){ + $r1 = $C['anti_link_spam'][1]; + if(!empty($r1) && preg_match($r1, $v)){continue;} + $r0 = $C['anti_link_spam'][0]; + if(!empty($r0) && preg_match($r0, $v)){ + if(isset($a['rel'])){ + if(!preg_match('`\bnofollow\b`i', $a['rel'])){$a['rel'] .= ' nofollow';} + }elseif(isset($aA['rel'])){ + if(!preg_match('`\bnofollow\b`i', $aA['rel'])){$nfr = 1;} + }else{$a['rel'] = 'nofollow';} + } + } + } + } + if(isset($rl[$k]) && is_array($rl[$k]) && ($v = hl_attrval($v, $rl[$k])) === 0){continue;} + $a[$k] = str_replace('"', '"', $v); + } +} +if($nfr){$a['rel'] = isset($a['rel']) ? $a['rel']. ' nofollow' : 'nofollow';} + +// rqd attr +static $eAR = array('area'=>array('alt'=>'area'), 'bdo'=>array('dir'=>'ltr'), 'form'=>array('action'=>''), 'img'=>array('src'=>'', 'alt'=>'image'), 'map'=>array('name'=>''), 'optgroup'=>array('label'=>''), 'param'=>array('name'=>''), 'script'=>array('type'=>'text/javascript'), 'textarea'=>array('rows'=>'10', 'cols'=>'50')); +if(isset($eAR[$e])){ + foreach($eAR[$e] as $k=>$v){ + if(!isset($a[$k])){$a[$k] = isset($v[0]) ? $v : $k;} + } +} + +// depr attrs +if($depTr){ + $c = array(); + foreach($a as $k=>$v){ + if($k == 'style' or !isset($aND[$k][$e])){continue;} + if($k == 'align'){ + unset($a['align']); + if($e == 'img' && ($v == 'left' or $v == 'right')){$c[] = 'float: '. $v;} + elseif(($e == 'div' or $e == 'table') && $v == 'center'){$c[] = 'margin: auto';} + else{$c[] = 'text-align: '. $v;} + }elseif($k == 'bgcolor'){ + unset($a['bgcolor']); + $c[] = 'background-color: '. $v; + }elseif($k == 'border'){ + unset($a['border']); $c[] = "border: {$v}px"; + }elseif($k == 'bordercolor'){ + unset($a['bordercolor']); $c[] = 'border-color: '. $v; + }elseif($k == 'clear'){ + unset($a['clear']); $c[] = 'clear: '. ($v != 'all' ? $v : 'both'); + }elseif($k == 'compact'){ + unset($a['compact']); $c[] = 'font-size: 85%'; + }elseif($k == 'height' or $k == 'width'){ + unset($a[$k]); $c[] = $k. ': '. ($v[0] != '*' ? $v. (ctype_digit($v) ? 'px' : '') : 'auto'); + }elseif($k == 'hspace'){ + unset($a['hspace']); $c[] = "margin-left: {$v}px; margin-right: {$v}px"; + }elseif($k == 'language' && !isset($a['type'])){ + unset($a['language']); + $a['type'] = 'text/'. strtolower($v); + }elseif($k == 'name'){ + if($C['no_deprecated_attr'] == 2 or ($e != 'a' && $e != 'map')){unset($a['name']);} + if(!isset($a['id']) && preg_match('`[a-zA-Z][a-zA-Z\d.:_\-]*`', $v)){$a['id'] = $v;} + }elseif($k == 'noshade'){ + unset($a['noshade']); $c[] = 'border-style: none; border: 0; background-color: gray; color: gray'; + }elseif($k == 'nowrap'){ + unset($a['nowrap']); $c[] = 'white-space: nowrap'; + }elseif($k == 'size'){ + unset($a['size']); $c[] = 'size: '. $v. 'px'; + }elseif($k == 'start' or $k == 'value'){ + unset($a[$k]); + }elseif($k == 'type'){ + unset($a['type']); + static $ol_type = array('i'=>'lower-roman', 'I'=>'upper-roman', 'a'=>'lower-latin', 'A'=>'upper-latin', '1'=>'decimal'); + $c[] = 'list-style-type: '. (isset($ol_type[$v]) ? $ol_type[$v] : 'decimal'); + }elseif($k == 'vspace'){ + unset($a['vspace']); $c[] = "margin-top: {$v}px; margin-bottom: {$v}px"; + } + } + if(count($c)){ + $c = implode('; ', $c); + $a['style'] = isset($a['style']) ? rtrim($a['style'], ' ;'). '; '. $c. ';': $c. ';'; + } +} +// unique ID +if($C['unique_ids'] && isset($a['id'])){ + if(!preg_match('`^[A-Za-z][A-Za-z0-9_\-.:]*$`', ($id = $a['id'])) or (isset($GLOBALS['hl_Ids'][$id]) && $C['unique_ids'] == 1)){unset($a['id']); + }else{ + while(isset($GLOBALS['hl_Ids'][$id])){$id = $C['unique_ids']. $id;} + $GLOBALS['hl_Ids'][($a['id'] = $id)] = 1; + } +} +// xml:lang +if($C['xml:lang'] && isset($a['lang'])){ + $a['xml:lang'] = isset($a['xml:lang']) ? $a['xml:lang'] : $a['lang']; + if($C['xml:lang'] == 2){unset($a['lang']);} +} +// for transformed tag +if(!empty($trt)){ + $a['style'] = isset($a['style']) ? rtrim($a['style'], ' ;'). '; '. $trt : $trt; +} +// return with empty ele / +if(empty($C['hook_tag'])){ + $aA = ''; + foreach($a as $k=>$v){$aA .= " {$k}=\"{$v}\"";} + return "<{$e}{$aA}". (isset($eE[$e]) ? ' /' : ''). '>'; +} +else{return $C['hook_tag']($e, $a);} +// eof +} + +function hl_tag2(&$e, &$a, $t=1){ +// transform tag +if($e == 'center'){$e = 'div'; return 'text-align: center;';} +if($e == 'dir' or $e == 'menu'){$e = 'ul'; return '';} +if($e == 's' or $e == 'strike'){$e = 'span'; return 'text-decoration: line-through;';} +if($e == 'u'){$e = 'span'; return 'text-decoration: underline;';} +static $fs = array('0'=>'xx-small', '1'=>'xx-small', '2'=>'small', '3'=>'medium', '4'=>'large', '5'=>'x-large', '6'=>'xx-large', '7'=>'300%', '-1'=>'smaller', '-2'=>'60%', '+1'=>'larger', '+2'=>'150%', '+3'=>'200%', '+4'=>'300%'); +if($e == 'font'){ + $a2 = ''; + if(preg_match('`face\s*=\s*(\'|")([^=]+?)\\1`i', $a, $m) or preg_match('`face\s*=\s*([^"])(\S+)`i', $a, $m)){ + $a2 .= ' font-family: '. str_replace('"', '\'', trim($m[2])). ';'; + } + if(preg_match('`color\s*=\s*(\'|")?(.+?)(\\1|\s|$)`i', $a, $m)){ + $a2 .= ' color: '. trim($m[2]). ';'; + } + if(preg_match('`size\s*=\s*(\'|")?(.+?)(\\1|\s|$)`i', $a, $m) && isset($fs[($m = trim($m[2]))])){ + $a2 .= ' font-size: '. $fs[$m]. ';'; + } + $e = 'span'; return ltrim($a2); +} +if($t == 2){$e = 0; return 0;} +return ''; +// eof +} + +function hl_tidy($t, $w, $p){ +// Tidy/compact HTM +if(strpos(' pre,script,textarea', "$p,")){return $t;} +$t = str_replace(' </', '</', preg_replace(array('`(<\w[^>]*(?<!/)>)\s+`', '`\s+`', '`(<\w[^>]*(?<!/)>) `'), array(' $1', ' ', '$1'), preg_replace_callback(array('`(<(!\[CDATA\[))(.+?)(\]\]>)`sm', '`(<(!--))(.+?)(-->)`sm', '`(<(pre|script|textarea)[^>]*?>)(.+?)(</\2>)`sm'), create_function('$m', 'return $m[1]. str_replace(array("<", ">", "\n", "\r", "\t", " "), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), $m[3]). $m[4];'), $t))); +if(($w = strtolower($w)) == -1){ + return str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), array('<', '>', "\n", "\r", "\t", ' '), $t); +} +$s = strpos(" $w", 't') ? "\t" : ' '; +$s = preg_match('`\d`', $w, $m) ? str_repeat($s, $m[0]) : str_repeat($s, ($s == "\t" ? 1 : 2)); +$n = preg_match('`[ts]([1-9])`', $w, $m) ? $m[1] : 0; +$a = array('br'=>1); +$b = array('button'=>1, 'input'=>1, 'option'=>1); +$c = array('caption'=>1, 'dd'=>1, 'dt'=>1, 'h1'=>1, 'h2'=>1, 'h3'=>1, 'h4'=>1, 'h5'=>1, 'h6'=>1, 'isindex'=>1, 'label'=>1, 'legend'=>1, 'li'=>1, 'object'=>1, 'p'=>1, 'pre'=>1, 'td'=>1, 'textarea'=>1, 'th'=>1); +$d = array('address'=>1, 'blockquote'=>1, 'center'=>1, 'colgroup'=>1, 'dir'=>1, 'div'=>1, 'dl'=>1, 'fieldset'=>1, 'form'=>1, 'hr'=>1, 'iframe'=>1, 'map'=>1, 'menu'=>1, 'noscript'=>1, 'ol'=>1, 'optgroup'=>1, 'rbc'=>1, 'rtc'=>1, 'ruby'=>1, 'script'=>1, 'select'=>1, 'table'=>1, 'tfoot'=>1, 'thead'=>1, 'tr'=>1, 'ul'=>1); +ob_start(); +if(isset($d[$p])){echo str_repeat($s, ++$n);} +$t = explode('<', $t); +echo ltrim(array_shift($t)); +for($i=-1, $j=count($t); ++$i<$j;){ + $r = ''; list($e, $r) = explode('>', $t[$i]); + $x = $e[0] == '/' ? 0 : (substr($e, -1) == '/' ? 1 : ($e[0] != '!' ? 2 : -1)); + $y = !$x ? ltrim($e, '/') : ($x > 0 ? substr($e, 0, strcspn($e, ' ')) : 0); + $e = "<$e>"; + if(isset($d[$y])){ + if(!$x){echo "\n", str_repeat($s, --$n), "$e\n", str_repeat($s, $n);} + else{echo "\n", str_repeat($s, $n), "$e\n", str_repeat($s, ($x != 1 ? ++$n : $n));} + echo ltrim($r); continue; + } + $f = "\n". str_repeat($s, $n); + if(isset($c[$y])){ + if(!$x){echo $e, $f, ltrim($r);} + else{echo $f, $e, $r;} + }elseif(isset($b[$y])){echo $f, $e, $r; + }elseif(isset($a[$y])){echo $e, $f, ltrim($r); + }elseif(!$y){echo $f, $e, $f, ltrim($r); + }else{echo $e, $r;} +} +$t = preg_replace('`[\n]\s*?[\n]+`', "\n", ob_get_contents()); +ob_end_clean(); +if(($l = strpos(" $w", 'r') ? (strpos(" $w", 'n') ? "\r\n" : "\r") : 0)){ + $t = str_replace("\n", $l, $t); +} +return str_replace(array("\x01", "\x02", "\x03", "\x04", "\x05", "\x07"), array('<', '>', "\n", "\r", "\t", ' '), $t); +// eof +} + +function hl_version(){ +// rel +return '1.1.10'; +// eof +} + +function kses($t, $h, $p=array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'gopher', 'mailto')){ +// kses compat +foreach($h as $k=>$v){ + $h[$k]['n']['*'] = 1; +} +$C['cdata'] = $C['comment'] = $C['make_tag_strict'] = $C['no_deprecated_attr'] = $C['unique_ids'] = 0; +$C['keep_bad'] = 1; +$C['elements'] = count($h) ? strtolower(implode(',', array_keys($h))) : '-*'; +$C['hook'] = 'kses_hook'; +$C['schemes'] = '*:'. implode(',', $p); +return htmLawed($t, $C, $h); +// eof +} + +function kses_hook($t, &$C, &$S){ +// kses compat +return $t; +// eof +} \ No newline at end of file diff --git a/include/index.php b/include/index.php new file mode 100644 index 0000000000000000000000000000000000000000..f9518a67ebc4a50b4c9b59f7920221c245a6b0dc --- /dev/null +++ b/include/index.php @@ -0,0 +1,3 @@ +<?php +header('Location: ../'); +?> diff --git a/include/mysql.php b/include/mysql.php new file mode 100644 index 0000000000000000000000000000000000000000..192859af970722c9127e1c57c7596085e98290f5 --- /dev/null +++ b/include/mysql.php @@ -0,0 +1,209 @@ +<?php +/********************************************************************* + mysql.php + + Collection of MySQL helper interface functions. + + Mostly wrappers with error/resource checking. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + + require_once(INCLUDE_DIR.'class.sys.php'); + + function db_connect($host, $user, $passwd, $db = "") { + + //Assert + if(!strlen($user) || !strlen($passwd) || !strlen($host)) + return NULL; + + //Connect + if(!($dblink =@mysql_connect($host, $user, $passwd))) + return NULL; + + //Select the database, if any. + if($db) db_select_database($db); + + //set desired encoding just in case mysql charset is not UTF-8 - Thanks to FreshMedia + @mysql_query('SET NAMES "UTF8"'); + @mysql_query('SET COLLATION_CONNECTION=utf8_general_ci'); + + return $dblink; + } + + function db_close(){ + global $dblink; + return @mysql_close($dblink); + } + + function db_version(){ + + $version=0; + if(preg_match('/(\d{1,2}\.\d{1,2}\.\d{1,2})/', mysql_result(db_query('SELECT VERSION()'),0,0),$matches)) + $version=$matches[1]; + + return $version; + } + + function db_get_variable($variable, $type='session') { + $sql =sprintf('SELECT @@%s.%s',$type,$variable); + return db_result(db_query($sql)); + } + + function db_set_variable($variable, $value, $type='session') { + $sql =sprintf('SET %s %s=%s',strtoupper($type), $variable, db_input($value)); + return db_query($sql); + } + + + function db_select_database($database) { + return ($database && @mysql_select_db($database)); + } + + function db_create_database($database, $charset='utf8', $collate='utf8_unicode_ci') { + return @mysql_query(sprintf('CREATE DATABASE %s DEFAULT CHARACTER SET %s COLLATE %s',$database,$charset,$collate)); + } + + // execute sql query + function db_query($query, $database="",$conn=""){ + global $cfg; + + if($conn) { /* connection is provided*/ + $result = ($database)?mysql_db_query($database,$query,$conn):mysql_query($query,$conn); + } else { + $result = ($database)?mysql_db_query($database,$query):mysql_query($query); + } + + if(!$result) { //error reporting + $alert='['.$query.']'."\n\n".db_error(); + Sys::log(LOG_ALERT,'DB Error #'.db_errno(),$alert,($cfg && $cfg->alertONSQLError())); + //echo $alert; #uncomment during debuging or dev. + } + + return $result; + } + + function db_squery($query){ //smart db query...utilizing args and sprintf + + $args = func_get_args(); + $query = array_shift($args); + $query = str_replace("?", "%s", $query); + $args = array_map('db_real_escape', $args); + array_unshift($args,$query); + $query = call_user_func_array('sprintf',$args); + return db_query($query); + } + + function db_count($query){ + return db_result(db_query($query)); + } + + function db_result($result,$row=0) { + return ($result)?mysql_result($result,$row):NULL; + } + + function db_fetch_array($result,$mode=false) { + return ($result)?db_output(mysql_fetch_array($result,($mode)?$mode:MYSQL_ASSOC)):NULL; + } + + function db_fetch_row($result) { + return ($result)?db_output(mysql_fetch_row($result)):NULL; + } + + function db_fetch_field($result) { + return ($result)?mysql_fetch_field($result):NULL; + } + + function db_assoc_array($result,$mode=false) { + if($result && db_num_rows($result)) { + while ($row=db_fetch_array($result,$mode)) + $results[]=$row; + } + return $results; + } + + function db_num_rows($result) { + return ($result)?mysql_num_rows($result):0; + } + + function db_affected_rows() { + return mysql_affected_rows(); + } + + function db_data_seek($result, $row_number) { + return mysql_data_seek($result, $row_number); + } + + function db_data_reset($result) { + return mysql_data_seek($result,0); + } + + function db_insert_id() { + return mysql_insert_id(); + } + + function db_free_result($result) { + return mysql_free_result($result); + } + + function db_output($param) { + + if(!function_exists('get_magic_quotes_runtime') || !get_magic_quotes_runtime()) //Sucker is NOT on - thanks. + return $param; + + if (is_array($param)) { + reset($param); + while(list($key, $value) = each($param)) + $param[$key] = db_output($value); + + return $param; + + }elseif(!is_numeric($param)) { + $param=trim(stripslashes($param)); + } + + return $param; + } + + //Do not call this function directly...use db_input + function db_real_escape($val,$quote=false){ + + //Magic quotes crap is taken care of in main.inc.php + $val=mysql_real_escape_string($val); + + return ($quote)?"'$val'":$val; + } + + function db_input($param,$quote=true) { + + //is_numeric doesn't work all the time...9e8 is considered numeric..which is correct...but not expected. + if($param && preg_match("/^\d+(\.\d+)?$/",$param)) + return $param; + + if($param && is_array($param)) { + reset($param); + while (list($key, $value) = each($param)) { + $param[$key] = db_input($value,$quote); + } + + return $param; + } + + return db_real_escape($param,$quote); + } + + function db_error(){ + return mysql_error(); + } + + function db_errno(){ + return mysql_errno(); + } +?> diff --git a/include/ost-sampleconfig.php b/include/ost-sampleconfig.php new file mode 100644 index 0000000000000000000000000000000000000000..8a1f3b98eed8b51f6cbd535a8c181bebb5b7f312 --- /dev/null +++ b/include/ost-sampleconfig.php @@ -0,0 +1,47 @@ +<?php +/********************************************************************* + ost-config.php + + Static osTicket configuration file. Mainly useful for mysql login info. + Created during installation process and shouldn't change even on upgrades. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2010 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: + $Id: $ +**********************************************************************/ + +#Disable direct access. +if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__)) || !defined('ROOT_PATH')) die('kwaheri rafiki!'); + +#Install flag +define('OSTINSTALLED',FALSE); +if(OSTINSTALLED!=TRUE){ + if(!file_exists(ROOT_PATH.'setup/install.php')) die('Error: Contact system admin.'); //Something is really wrong! + //Invoke the installer. + header('Location: '.ROOT_PATH.'setup/install.php'); + exit; +} + +# Encrypt/Decrypt secret key - randomly generated during installation. +define('SECRET_SALT','%CONFIG-SIRI'); + +#Default admin email. Used only on db connection issues and related alerts. +define('ADMIN_EMAIL','%ADMIN-EMAIL'); + +#Mysql Login info +define('DBTYPE','mysql'); +define('DBHOST','%CONFIG-DBHOST'); +define('DBNAME','%CONFIG-DBNAME'); +define('DBUSER','%CONFIG-DBUSER'); +define('DBPASS','%CONFIG-DBPASS'); + +#Table prefix +define('TABLE_PREFIX','%CONFIG-PREFIX'); + +?> diff --git a/include/pear/Auth/SASL.php b/include/pear/Auth/SASL.php new file mode 100644 index 0000000000000000000000000000000000000000..45a3f713d2e2444ed1718d7a40f2591c081209d4 --- /dev/null +++ b/include/pear/Auth/SASL.php @@ -0,0 +1,99 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: SASL.php,v 1.5 2006/03/22 05:20:11 amistry Exp $ + +/** +* Client implementation of various SASL mechanisms +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('PEAR.php'); + +class Auth_SASL +{ + /** + * Factory class. Returns an object of the request + * type. + * + * @param string $type One of: Anonymous + * Plain + * CramMD5 + * DigestMD5 + * Types are not case sensitive + */ + function &factory($type) + { + switch (strtolower($type)) { + case 'anonymous': + $filename = 'Auth/SASL/Anonymous.php'; + $classname = 'Auth_SASL_Anonymous'; + break; + + case 'login': + $filename = 'Auth/SASL/Login.php'; + $classname = 'Auth_SASL_Login'; + break; + + case 'plain': + $filename = 'Auth/SASL/Plain.php'; + $classname = 'Auth_SASL_Plain'; + break; + + case 'crammd5': + $filename = 'Auth/SASL/CramMD5.php'; + $classname = 'Auth_SASL_CramMD5'; + break; + + case 'digestmd5': + $filename = 'Auth/SASL/DigestMD5.php'; + $classname = 'Auth_SASL_DigestMD5'; + break; + + default: + return PEAR::raiseError('Invalid SASL mechanism type'); + break; + } + + require_once($filename); + $obj = new $classname(); + return $obj; + } +} + +?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/Anonymous.php b/include/pear/Auth/SASL/Anonymous.php new file mode 100644 index 0000000000000000000000000000000000000000..dc6511c1af248645664d969b0868f02e93b8bda3 --- /dev/null +++ b/include/pear/Auth/SASL/Anonymous.php @@ -0,0 +1,71 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: Anonymous.php,v 1.4 2003/02/21 16:07:17 mj Exp $ + +/** +* Implmentation of ANONYMOUS SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Anonymous extends Auth_SASL_Common +{ + /** + * Not much to do here except return the token supplied. + * No encoding, hashing or encryption takes place for this + * mechanism, simply one of: + * o An email address + * o An opaque string not containing "@" that can be interpreted + * by the sysadmin + * o Nothing + * + * We could have some logic here for the second option, but this + * would by no means create something interpretable. + * + * @param string $token Optional email address or string to provide + * as trace information. + * @return string The unaltered input token + */ + function getResponse($token = '') + { + return $token; + } +} +?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/Common.php b/include/pear/Auth/SASL/Common.php new file mode 100644 index 0000000000000000000000000000000000000000..f21c8cc1ce55ea7548e2a88ce96efb446ea715f7 --- /dev/null +++ b/include/pear/Auth/SASL/Common.php @@ -0,0 +1,74 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.6 2003/02/21 16:07:17 mj Exp $ + +/** +* Common functionality to SASL mechanisms +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +class Auth_SASL_Common +{ + /** + * Function which implements HMAC MD5 digest + * + * @param string $key The secret key + * @param string $data The data to protect + * @return string The HMAC MD5 digest + */ + function _HMAC_MD5($key, $data) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } + + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $data)); + $digest = md5($k_opad . $inner); + + return $digest; + } +} +?> diff --git a/include/pear/Auth/SASL/CramMD5.php b/include/pear/Auth/SASL/CramMD5.php new file mode 100644 index 0000000000000000000000000000000000000000..086248fee8b6d6f63d1df55d7a44d308500bb1cd --- /dev/null +++ b/include/pear/Auth/SASL/CramMD5.php @@ -0,0 +1,68 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: CramMD5.php,v 1.4 2003/02/21 16:07:17 mj Exp $ + +/** +* Implmentation of CRAM-MD5 SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_CramMD5 extends Auth_SASL_Common +{ + /** + * Implements the CRAM-MD5 SASL mechanism + * This DOES NOT base64 encode the return value, + * you will need to do that yourself. + * + * @param string $user Username + * @param string $pass Password + * @param string $challenge The challenge supplied by the server. + * this should be already base64_decoded. + * + * @return string The string to pass back to the server, of the form + * "<user> <digest>". This is NOT base64_encoded. + */ + function getResponse($user, $pass, $challenge) + { + return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); + } +} +?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/DigestMD5.php b/include/pear/Auth/SASL/DigestMD5.php new file mode 100644 index 0000000000000000000000000000000000000000..4534e500bb35712acc25b87f4272f6ba39132e42 --- /dev/null +++ b/include/pear/Auth/SASL/DigestMD5.php @@ -0,0 +1,198 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: DigestMD5.php,v 1.8 2006/03/22 05:20:11 amistry Exp $ + +/** +* Implmentation of DIGEST-MD5 SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_DigestMD5 extends Auth_SASL_Common +{ + /** + * Provides the (main) client response for DIGEST-MD5 + * requires a few extra parameters than the other + * mechanisms, which are unavoidable. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The digest challenge sent by the server + * @param string $hostname The hostname of the machine you're connecting to + * @param string $service The servicename (eg. imap, pop, acap etc) + * @param string $authzid Authorization id (username to proxy as) + * @return string The digest response (NOT base64 encoded) + * @access public + */ + function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '') + { + $challenge = $this->_parseChallenge($challenge); + $authzid_string = ''; + if ($authzid != '') { + $authzid_string = ',authzid="' . $authzid . '"'; + } + + if (!empty($challenge)) { + $cnonce = $this->_getCnonce(); + $digest_uri = sprintf('%s/%s', $service, $hostname); + $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); + + if ($challenge['realm']) { + return sprintf('username="%s",realm="%s"' . $authzid_string . +',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } else { + return sprintf('username="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } + } else { + return PEAR::raiseError('Invalid digest challenge'); + } + } + + /** + * Parses and verifies the digest challenge* + * + * @param string $challenge The digest challenge + * @return array The parsed challenge as an assoc + * array in the form "directive => value". + * @access private + */ + function _parseChallenge($challenge) + { + $tokens = array(); + while (preg_match('/^([a-z-]+)=("[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches)) { + + // Ignore these as per rfc2831 + if ($matches[1] == 'opaque' OR $matches[1] == 'domain') { + $challenge = substr($challenge, strlen($matches[0]) + 1); + continue; + } + + // Allowed multiple "realm" and "auth-param" + if (!empty($tokens[$matches[1]]) AND ($matches[1] == 'realm' OR $matches[1] == 'auth-param')) { + if (is_array($tokens[$matches[1]])) { + $tokens[$matches[1]][] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]); + } else { + $tokens[$matches[1]] = array($tokens[$matches[1]], preg_replace('/^"(.*)"$/', '\\1', $matches[2])); + } + + // Any other multiple instance = failure + } elseif (!empty($tokens[$matches[1]])) { + $tokens = array(); + break; + + } else { + $tokens[$matches[1]] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]); + } + + // Remove the just parsed directive from the challenge + $challenge = substr($challenge, strlen($matches[0]) + 1); + } + + /** + * Defaults and required directives + */ + // Realm + if (empty($tokens['realm'])) { + $tokens['realm'] = ""; + } + + // Maxbuf + if (empty($tokens['maxbuf'])) { + $tokens['maxbuf'] = 65536; + } + + // Required: nonce, algorithm + if (empty($tokens['nonce']) OR empty($tokens['algorithm'])) { + return array(); + } + + return $tokens; + } + + /** + * Creates the response= part of the digest response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $realm Realm as provided by the server + * @param string $nonce Nonce as provided by the server + * @param string $cnonce Client nonce + * @param string $digest_uri The digest-uri= value part of the response + * @param string $authzid Authorization id + * @return string The response= part of the digest response + * @access private + */ + function _getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '') + { + if ($authzid == '') { + $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce); + } else { + $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce, $authzid); + } + $A2 = 'AUTHENTICATE:' . $digest_uri; + return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2))); + } + + /** + * Creates the client nonce for the response + * + * @return string The cnonce value + * @access private + */ + function _getCnonce() + { + if (file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) { + return base64_encode(fread($fd, 32)); + + } elseif (file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) { + return base64_encode(fread($fd, 32)); + + } else { + $str = ''; + mt_srand((double)microtime()*10000000); + for ($i=0; $i<32; $i++) { + $str .= chr(mt_rand(0, 255)); + } + + return base64_encode($str); + } + } +} +?> diff --git a/include/pear/Auth/SASL/Login.php b/include/pear/Auth/SASL/Login.php new file mode 100644 index 0000000000000000000000000000000000000000..bad9ab98c3d10e5869f50826f0463bc80fa428a6 --- /dev/null +++ b/include/pear/Auth/SASL/Login.php @@ -0,0 +1,65 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: Login.php,v 1.4 2003/02/21 16:07:17 mj Exp $ + +/** +* This is technically not a SASL mechanism, however +* it's used by Net_Sieve, Net_Cyrus and potentially +* other protocols , so here is a good place to abstract +* it. +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Login extends Auth_SASL_Common +{ + /** + * Pseudo SASL LOGIN mechanism + * + * @param string $user Username + * @param string $pass Password + * @return string LOGIN string + */ + function getResponse($user, $pass) + { + return sprintf('LOGIN %s %s', $user, $pass); + } +} +?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/Plain.php b/include/pear/Auth/SASL/Plain.php new file mode 100644 index 0000000000000000000000000000000000000000..e4662ff700269d6eff59f509118a4f0b34c7130b --- /dev/null +++ b/include/pear/Auth/SASL/Plain.php @@ -0,0 +1,63 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id: Plain.php,v 1.6 2003/09/11 18:53:56 mbretter Exp $ + +/** +* Implmentation of PLAIN SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Plain extends Auth_SASL_Common +{ + /** + * Returns PLAIN response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string PLAIN Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid . chr(0) . $authcid . chr(0) . $pass; + } +} +?> diff --git a/include/pear/Mail.php b/include/pear/Mail.php new file mode 100644 index 0000000000000000000000000000000000000000..7a3c70f82190e89749f7651b8d3b8b4fc4748681 --- /dev/null +++ b/include/pear/Mail.php @@ -0,0 +1,245 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: Mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ + +require_once 'PEAR.php'; + +/** + * PEAR's Mail:: interface. Defines the interface for implementing + * mailers under the PEAR hierarchy, and provides supporting functions + * useful in multiple mailer backends. + * + * @access public + * @version $Revision: 1.20 $ + * @package Mail + */ +class Mail +{ + /** + * Line terminator used for separating header lines. + * @var string + */ + var $sep = "\r\n"; + + /** + * Provides an interface for generating Mail:: objects of various + * types + * + * @param string $driver The kind of Mail:: object to instantiate. + * @param array $params The parameters to pass to the Mail:: object. + * @return object Mail a instance of the driver class or if fails a PEAR Error + * @access public + */ + function &factory($driver, $params = array()) + { + $driver = strtolower($driver); + include_once 'Mail/' . $driver . '.php'; + $class = 'Mail_' . $driver; + if (class_exists($class)) { + $mailer = new $class($params); + return $mailer; + } else { + return PEAR::raiseError('Unable to find class for driver ' . $driver); + } + } + + /** + * Implements Mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + * @deprecated use Mail_mail::send instead + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + // if we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // flatten the headers out. + list(, $text_headers) = Mail::prepareHeaders($headers); + + return mail($recipients, $subject, $body, $text_headers); + } + + /** + * Sanitize an array of mail headers by removing any additional header + * strings present in a legitimate header's value. The goal of this + * filter is to prevent mail injection attacks. + * + * @param array $headers The associative array of headers to sanitize. + * + * @access private + */ + function _sanitizeHeaders(&$headers) + { + foreach ($headers as $key => $value) { + $headers[$key] = + preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', + null, $value); + } + } + + /** + * Take an array of mail headers and return a string containing + * text usable in sending a message. + * + * @param array $headers The array of headers to prepare, in an associative + * array, where the array key is the header name (ie, + * 'Subject'), and the array value is the header + * value (ie, 'test'). The header produced from those + * values would be 'Subject: test'. + * + * @return mixed Returns false if it encounters a bad address, + * otherwise returns an array containing two + * elements: Any From: address found in the headers, + * and the plain text version of the headers. + * @access private + */ + function prepareHeaders($headers) + { + $lines = array(); + $from = null; + + foreach ($headers as $key => $value) { + if (strcasecmp($key, 'From') === 0) { + include_once 'Mail/RFC822.php'; + $parser = new Mail_RFC822(); + $addresses = $parser->parseAddressList($value, 'localhost', false); + if (is_a($addresses, 'PEAR_Error')) { + return $addresses; + } + + $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; + + // Reject envelope From: addresses with spaces. + if (strstr($from, ' ')) { + return false; + } + + $lines[] = $key . ': ' . $value; + } elseif (strcasecmp($key, 'Received') === 0) { + $received = array(); + if (is_array($value)) { + foreach ($value as $line) { + $received[] = $key . ': ' . $line; + } + } + else { + $received[] = $key . ': ' . $value; + } + // Put Received: headers at the top. Spam detectors often + // flag messages with Received: headers after the Subject: + // as spam. + $lines = array_merge($received, $lines); + } else { + // If $value is an array (i.e., a list of addresses), convert + // it to a comma-delimited string of its elements (addresses). + if (is_array($value)) { + $value = implode(', ', $value); + } + $lines[] = $key . ': ' . $value; + } + } + + return array($from, join($this->sep, $lines)); + } + + /** + * Take a set of recipients and parse them, returning an array of + * bare addresses (forward paths) that can be passed to sendmail + * or an smtp server with the rcpt to: command. + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. + * + * @return mixed An array of forward paths (bare addresses) or a PEAR_Error + * object if the address list could not be parsed. + * @access private + */ + function parseRecipients($recipients) + { + include_once 'Mail/RFC822.php'; + + // if we're passed an array, assume addresses are valid and + // implode them before parsing. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Parse recipients, leaving out all personal info. This is + // for smtp recipients, etc. All relevant personal information + // should already be in the headers. + $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false); + + // If parseAddressList() returned a PEAR_Error object, just return it. + if (is_a($addresses, 'PEAR_Error')) { + return $addresses; + } + + $recipients = array(); + if (is_array($addresses)) { + foreach ($addresses as $ob) { + $recipients[] = $ob->mailbox . '@' . $ob->host; + } + } + + return $recipients; + } + +} diff --git a/include/pear/Mail/RFC822.php b/include/pear/Mail/RFC822.php new file mode 100644 index 0000000000000000000000000000000000000000..866638268dad6d6254280b301d9057056cbffc62 --- /dev/null +++ b/include/pear/Mail/RFC822.php @@ -0,0 +1,940 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2001-2002, Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Authors: Richard Heyes <richard@phpguru.org> | +// | Chuck Hagenbuch <chuck@horde.org> | +// +-----------------------------------------------------------------------+ + +/** + * RFC 822 Email address list validation Utility + * + * What is it? + * + * This class will take an address string, and parse it into it's consituent + * parts, be that either addresses, groups, or combinations. Nested groups + * are not supported. The structure it returns is pretty straight forward, + * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use + * print_r() to view the structure. + * + * How do I use it? + * + * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes <richard@phpguru.org> + * @author Chuck Hagenbuch <chuck@horde.org> + * @version $Revision: 1.24 $ + * @license BSD + * @package Mail + */ +class Mail_RFC822 { + + /** + * The address being parsed by the RFC822 object. + * @var string $address + */ + var $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @var string $default_domain + */ + var $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @var boolean $nestGroups + */ + var $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @var boolean $validate + */ + var $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @var array $addresses + */ + var $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @var array $structure + */ + var $structure = array(); + + /** + * The current error message, if any. + * @var string $error + */ + var $error = null; + + /** + * An internal counter/pointer. + * @var integer $index + */ + var $index = null; + + /** + * The number of groups that have been found in the address list. + * @var integer $num_groups + * @access public + */ + var $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @var boolean $mailRFC822 + */ + var $mailRFC822 = true; + + /** + * A limit after which processing stops + * @var int $limit + */ + var $limit = null; + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (!isset($this) || !isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + // Unfold any long lines in $this->address. + $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); + $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); + + while ($this->address = $this->_splitAddresses($this->address)); + + if ($this->address === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + // Validate each address individually. If we encounter an invalid + // address, stop iterating and return an error immediately. + foreach ($this->addresses as $address) { + $valid = $this->_validateAddress($address); + + if ($valid === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $valid); + } else { + $this->structure[] = $valid; + } + } + + return $this->structure; + } + + /** + * Splits an address into separate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + if (!empty($this->limit) && count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) { + return false; + } + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has unclosed quotes or not. + * + * @access private + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, + * false otherwise. + */ + function _hasUnclosedQuotes($string) + { + $string = trim($string); + $iMax = strlen($string); + $in_quote = false; + $i = $slashes = 0; + + for (; $i < $iMax; ++$i) { + switch ($string[$i]) { + case '\\': + ++$slashes; + break; + + case '"': + if ($slashes % 2 == 0) { + $in_quote = !$in_quote; + } + // Fall through to default action below. + + default: + $slashes = 0; + break; + } + } + + return $in_quote; + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + $addresses = array(); + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!count($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + // Trim the whitespace from all of the address strings. + array_map('trim', $addresses); + + // Validate each mailbox. + // Format could be one of: name <geezer@domain.com> + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + for ($i = 0; $i < count($addresses); $i++) { + if (!$this->validateMailbox($addresses[$i])) { + if (empty($this->error)) { + $this->error = 'Validation failed for: ' . $addresses[$i]; + } + return false; + } + } + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // Splits on one or more Tab or space. + $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + + $phrase_parts = array(); + while (count($parts) > 0){ + $phrase_parts[] = $this->_splitCheck($parts, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + foreach ($phrase_parts as $part) { + // If quoted string: + if (substr($part, 0, 1) == '"') { + if (!$this->_validateQuotedString($part)) { + return false; + } + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($part)) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1*<any CHAR except specials, SPACE and CTLs> + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // Validation has been turned off; assume the atom is okay. + return true; + } + + // Check for any char from ASCII 0 - ASCII 127 + if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { + return false; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check, removing quoted characters first. + return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + $comments = array(); + + // Catch any RFC822 comments and store them separately. + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a (. + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +1 is for the trailing ) + $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + } else { + break; + } + } + + foreach ($comments as $comment) { + $mailbox = str_replace("($comment)", '', $mailbox); + } + + $mailbox = trim($mailbox); + + // Check for name + route-addr + if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { + return false; + } + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { + $addr_spec = substr($mailbox, 1, -1); + } else { + $addr_spec = $mailbox; + } + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + foreach ($domains as $domain) { + $domain = str_replace('@', '', trim($domain)); + if (!$this->_validateDomain($domain)) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + foreach ($sub_domains as $sub_domain) { + if (!$this->_validateSubdomain(trim($sub_domain))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + $words = array(); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + foreach ($words as $word) { + // If this word contains an unquoted space, it is invalid. (6.2.4) + if (strpos($word, ' ') && $word[0] !== '"') + { + return false; + } + + if ($this->_validatePhrase(trim($word)) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are in the + * given string. This is APPROXIMATE as it only splits based on a + * comma which has no preceding backslash. Could be useful as + * large amounts of addresses will end up producing *large* + * structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?<!\\\\),/', $data)); + } + + /** + * This is a email validating function separate to the rest of the + * class. It simply validates whether an email is of the common + * internet form: <user>@<domain>. This can be sufficient for most + * people. Optional stricter mode can be utilised which restricts + * mailbox characters allowed to alphanumeric, full stop, hyphen + * and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return false; + } + } + +} diff --git a/include/pear/Mail/mail.php b/include/pear/Mail/mail.php new file mode 100644 index 0000000000000000000000000000000000000000..b48d2697fa9acbad7b835ef8989dcb7fd9291297 --- /dev/null +++ b/include/pear/Mail/mail.php @@ -0,0 +1,143 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ + +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * @package Mail + * @version $Revision: 1.20 $ + */ +class Mail_mail extends Mail { + + /** + * Any arguments to pass to the mail() function. + * @var string + */ + var $_params = ''; + + /** + * Constructor. + * + * Instantiates a new Mail_mail:: object based on the parameters + * passed in. + * + * @param array $params Extra arguments for the mail() function. + */ + function Mail_mail($params = null) + { + // The other mail implementations accept parameters as arrays. + // In the interest of being consistent, explode an array into + // a string of parameter arguments. + if (is_array($params)) { + $this->_params = join(' ', $params); + } else { + $this->_params = $params; + } + + /* Because the mail() function may pass headers as command + * line arguments, we can't guarantee the use of the standard + * "\r\n" separator. Instead, we use the system's native line + * separator. */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail_mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + // If we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // Also remove the To: header. The mail() function will add its own + // To: header based on the contents of $recipients. + unset($headers['To']); + + // Flatten the headers out. + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list(, $text_headers) = $headerElements; + + // We only use mail()'s optional fifth parameter if the additional + // parameters have been provided and we're not running in safe mode. + if (empty($this->_params) || ini_get('safe_mode')) { + $result = mail($recipients, $subject, $body, $text_headers); + } else { + $result = mail($recipients, $subject, $body, $text_headers, + $this->_params); + } + + // If the mail() function returned failure, we need to create a + // PEAR_Error object and return it instead of the boolean result. + if ($result === false) { + $result = PEAR::raiseError('mail() returned failure'); + } + + return $result; + } + +} diff --git a/include/pear/Mail/mime.php b/include/pear/Mail/mime.php new file mode 100644 index 0000000000000000000000000000000000000000..2286920d76625b5fa1f9eb9537cd22bb4d8c5019 --- /dev/null +++ b/include/pear/Mail/mime.php @@ -0,0 +1,1095 @@ +<?php +/** + * The Mail_Mime class is used to create MIME E-mail messages + * + * The Mail_Mime class provides an OO interface to create MIME + * enabled email messages. This way you can create emails that + * contain plain-text bodies, HTML bodies, attachments, inline + * images and specific headers. + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mime.php,v 1.81 2007/06/21 19:08:28 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + * + * This class is based on HTML Mime Mail class from + * Richard Heyes <richard@phpguru.org> which was based also + * in the mime_mail.class by Tobias Ratschiller <tobias@dnet.it> + * and Sascha Schumann <sascha@schumann.cx> + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; + +/** + * require Mail_mimePart + * + * Mail_mimePart contains the code required to + * create all the different parts a mail can + * consist of. + */ +require_once 'Mail/mimePart.php'; + + +/** + * The Mail_Mime class provides an OO interface to create MIME + * enabled email messages. This way you can create emails that + * contain plain-text bodies, HTML bodies, attachments, inline + * images and specific headers. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mime +{ + /** + * Contains the plain text part of the email + * + * @var string + * @access private + */ + var $_txtbody; + + /** + * Contains the html part of the email + * + * @var string + * @access private + */ + var $_htmlbody; + + /** + * contains the mime encoded text + * + * @var string + * @access private + */ + var $_mime; + + /** + * contains the multipart content + * + * @var string + * @access private + */ + var $_multipart; + + /** + * list of the attached images + * + * @var array + * @access private + */ + var $_html_images = array(); + + /** + * list of the attachements + * + * @var array + * @access private + */ + var $_parts = array(); + + /** + * Build parameters + * + * @var array + * @access private + */ + var $_build_params = array(); + + /** + * Headers for the mail + * + * @var array + * @access private + */ + var $_headers = array(); + + /** + * End Of Line sequence (for serialize) + * + * @var string + * @access private + */ + var $_eol; + + + /** + * Constructor function. + * + * @param string $crlf what type of linebreak to use. + * Defaults to "\r\n" + * + * @return void + * + * @access public + */ + function Mail_mime($crlf = "\r\n") + { + $this->_setEOL($crlf); + $this->_build_params = array( + 'head_encoding' => 'quoted-printable', + 'text_encoding' => '7bit', + 'html_encoding' => 'quoted-printable', + '7bit_wrap' => 998, + 'html_charset' => 'ISO-8859-1', + 'text_charset' => 'ISO-8859-1', + 'head_charset' => 'ISO-8859-1' + ); + } + + /** + * wakeup function called by unserialize. It re-sets the EOL constant + * + * @access private + * @return void + */ + function __wakeup() + { + $this->_setEOL($this->_eol); + } + + + /** + * Accessor function to set the body text. Body text is used if + * it's not an html mail being sent or else is used to fill the + * text/plain part that emails clients who don't support + * html should show. + * + * @param string $data Either a string or + * the file name with the contents + * @param bool $isfile If true the first param should be treated + * as a file name, else as a string (default) + * @param bool $append If true the text or file is appended to + * the existing body, else the old body is + * overwritten + * + * @return mixed true on success or PEAR_Error object + * @access public + */ + function setTXTBody($data, $isfile = false, $append = false) + { + if (!$isfile) { + if (!$append) { + $this->_txtbody = $data; + } else { + $this->_txtbody .= $data; + } + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + if (!$append) { + $this->_txtbody = $cont; + } else { + $this->_txtbody .= $cont; + } + } + return true; + } + + /** + * Adds a html part to the mail. + * + * @param string $data either a string or the file name with the + * contents + * @param bool $isfile a flag that determines whether $data is a + * filename, or a string(false, default) + * + * @return bool true on success + * @access public + */ + function setHTMLBody($data, $isfile = false) + { + if (!$isfile) { + $this->_htmlbody = $data; + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + $this->_htmlbody = $cont; + } + + return true; + } + + /** + * Adds an image to the list of embedded images. + * + * @param string $file the image file name OR image data itself + * @param string $c_type the content type + * @param string $name the filename of the image. + * Only used if $file is the image data. + * @param bool $isfile whether $file is a filename or not. + * Defaults to true + * + * @return bool true on success + * @access public + */ + function addHTMLImage($file, $c_type='application/octet-stream', + $name = '', $isfile = true) + { + $filedata = ($isfile === true) ? $this->_file2str($file) + : $file; + if ($isfile === true) { + $filename = ($name == '' ? $file : $name); + } else { + $filename = $name; + } + if (PEAR::isError($filedata)) { + return $filedata; + } + $this->_html_images[] = array( + 'body' => $filedata, + 'name' => $filename, + 'c_type' => $c_type, + 'cid' => md5(uniqid(time())) + ); + return true; + } + + /** + * Adds a file to the list of attachments. + * + * @param string $file The file name of the file to attach + * OR the file contents itself + * @param string $c_type The content type + * @param string $name The filename of the attachment + * Only use if $file is the contents + * @param bool $isfile Whether $file is a filename or not + * Defaults to true + * @param string $encoding The type of encoding to use. + * Defaults to base64. + * Possible values: 7bit, 8bit, base64, + * or quoted-printable. + * @param string $disposition The content-disposition of this file + * Defaults to attachment. + * Possible values: attachment, inline. + * @param string $charset The character set used in the filename + * of this attachment. + * @param string $language The language of the attachment + * @param string $location The RFC 2557.4 location of the attachment + * + * @return mixed true on success or PEAR_Error object + * @access public + */ + function addAttachment($file, + $c_type = 'application/octet-stream', + $name = '', + $isfile = true, + $encoding = 'base64', + $disposition = 'attachment', + $charset = '', + $language = '', + $location = '') + { + $filedata = ($isfile === true) ? $this->_file2str($file) + : $file; + if ($isfile === true) { + // Force the name the user supplied, otherwise use $file + $filename = (strlen($name)) ? $name : $file; + } else { + $filename = $name; + } + if (!strlen($filename)) { + $msg = "The supplied filename for the attachment can't be empty"; + $err = PEAR::raiseError($msg); + return $err; + } + $filename = basename($filename); + if (PEAR::isError($filedata)) { + return $filedata; + } + + $this->_parts[] = array( + 'body' => $filedata, + 'name' => $filename, + 'c_type' => $c_type, + 'encoding' => $encoding, + 'charset' => $charset, + 'language' => $language, + 'location' => $location, + 'disposition' => $disposition + ); + return true; + } + + /** + * Get the contents of the given file name as string + * + * @param string $file_name path of file to process + * + * @return string contents of $file_name + * @access private + */ + function &_file2str($file_name) + { + if (!is_readable($file_name)) { + $err = PEAR::raiseError('File is not readable ' . $file_name); + return $err; + } + if (!$fd = fopen($file_name, 'rb')) { + $err = PEAR::raiseError('Could not open ' . $file_name); + return $err; + } + $filesize = filesize($file_name); + if ($filesize == 0) { + $cont = ""; + } else { + if ($magic_quote_setting = get_magic_quotes_runtime()) { + set_magic_quotes_runtime(0); + } + $cont = fread($fd, $filesize); + if ($magic_quote_setting) { + set_magic_quotes_runtime($magic_quote_setting); + } + } + fclose($fd); + return $cont; + } + + /** + * Adds a text subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * @param string $text The text to add. + * + * @return object The text mimePart object + * @access private + */ + function &_addTextPart(&$obj, $text) + { + $params['content_type'] = 'text/plain'; + $params['encoding'] = $this->_build_params['text_encoding']; + $params['charset'] = $this->_build_params['text_charset']; + if (is_object($obj)) { + $ret = $obj->addSubpart($text, $params); + return $ret; + } else { + $ret = new Mail_mimePart($text, $params); + return $ret; + } + } + + /** + * Adds a html subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The html mimePart object + * @access private + */ + function &_addHtmlPart(&$obj) + { + $params['content_type'] = 'text/html'; + $params['encoding'] = $this->_build_params['html_encoding']; + $params['charset'] = $this->_build_params['html_charset']; + if (is_object($obj)) { + $ret = $obj->addSubpart($this->_htmlbody, $params); + return $ret; + } else { + $ret = new Mail_mimePart($this->_htmlbody, $params); + return $ret; + } + } + + /** + * Creates a new mimePart object, using multipart/mixed as + * the initial content-type and returns it during the + * build process. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addMixedPart() + { + $params = array(); + $params['content_type'] = 'multipart/mixed'; + + //Create empty multipart/mixed Mail_mimePart object to return + $ret = new Mail_mimePart('', $params); + return $ret; + } + + /** + * Adds a multipart/alternative part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addAlternativePart(&$obj) + { + $params['content_type'] = 'multipart/alternative'; + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds a multipart/related part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addRelatedPart(&$obj) + { + $params['content_type'] = 'multipart/related'; + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds an html image subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The image information + * + * @return object The image mimePart object + * @access private + */ + function &_addHtmlImagePart(&$obj, $value) + { + $params['content_type'] = $value['c_type']; + $params['encoding'] = 'base64'; + $params['disposition'] = 'inline'; + $params['dfilename'] = $value['name']; + $params['cid'] = $value['cid']; + + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + + } + + /** + * Adds an attachment subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The attachment information + * + * @return object The image mimePart object + * @access private + */ + function &_addAttachmentPart(&$obj, $value) + { + $params['dfilename'] = $value['name']; + $params['encoding'] = $value['encoding']; + if ($value['charset']) { + $params['charset'] = $value['charset']; + } + if ($value['language']) { + $params['language'] = $value['language']; + } + if ($value['location']) { + $params['location'] = $value['location']; + } + $params['content_type'] = $value['c_type']; + $params['disposition'] = isset($value['disposition']) ? + $value['disposition'] : 'attachment'; + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + } + + /** + * Returns the complete e-mail, ready to send using an alternative + * mail delivery method. Note that only the mailpart that is made + * with Mail_Mime is created. This means that, + * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF + * using the $xtra_headers parameter! + * + * @param string $separation The separation etween these two parts. + * @param array $build_params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $xtra_headers The extra headers that should be passed + * to the &headers() function. + * See that function for more info. + * @param bool $overwrite Overwrite the existing headers with new. + * + * @return string The complete e-mail. + * @access public + */ + function getMessage( + $separation = null, + $build_params = null, + $xtra_headers = null, + $overwrite = false + ) + { + if ($separation === null) { + $separation = MAIL_MIME_CRLF; + } + $body = $this->get($build_params); + $head = $this->txtHeaders($xtra_headers, $overwrite); + $mail = $head . $separation . $body; + return $mail; + } + + + /** + * Builds the multipart message from the list ($this->_parts) and + * returns the mime content. + * + * @param array $build_params Build parameters that change the way the email + * is built. Should be associative. Can contain: + * head_encoding - What encoding to use for the headers. + * Options: quoted-printable or base64 + * Default is quoted-printable + * text_encoding - What encoding to use for plain text + * Options: 7bit, 8bit, + * base64, or quoted-printable + * Default is 7bit + * html_encoding - What encoding to use for html + * Options: 7bit, 8bit, + * base64, or quoted-printable + * Default is quoted-printable + * 7bit_wrap - Number of characters before text is + * wrapped in 7bit encoding + * Default is 998 + * html_charset - The character set to use for html. + * Default is iso-8859-1 + * text_charset - The character set to use for text. + * Default is iso-8859-1 + * head_charset - The character set to use for headers. + * Default is iso-8859-1 + * + * @return string The mime content + * @access public + */ + function &get($build_params = null) + { + if (isset($build_params)) { + while (list($key, $value) = each($build_params)) { + $this->_build_params[$key] = $value; + } + } + + if (isset($this->_headers['From'])){ + $domain = @strstr($this->_headers['From'],'@'); + //Bug #11381: Illegal characters in domain ID + $domain = str_replace(array("<", ">", "&", "(", ")", " ", "\"", "'"), "", $domain); + $domain = urlencode($domain); + foreach($this->_html_images as $i => $img){ + $this->_html_images[$i]['cid'] = $this->_html_images[$i]['cid'] . $domain; + } + } + + if (count($this->_html_images) AND isset($this->_htmlbody)) { + foreach ($this->_html_images as $key => $value) { + $regex = array(); + $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . + preg_quote($value['name'], '#') . '\3#'; + $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' . + preg_quote($value['name'], '#') . '\1\s*\)#'; + + $rep = array(); + $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3'; + $rep[] = 'url(\1cid:' . $value['cid'] . '\2)'; + + $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody); + $this->_html_images[$key]['name'] = + basename($this->_html_images[$key]['name']); + } + } + + $null = null; + $attachments = count($this->_parts) ? true : false; + $html_images = count($this->_html_images) ? true : false; + $html = strlen($this->_htmlbody) ? true : false; + $text = (!$html AND strlen($this->_txtbody)) ? true : false; + + switch (true) { + case $text AND !$attachments: + $message =& $this->_addTextPart($null, $this->_txtbody); + break; + + case !$text AND !$html AND $attachments: + $message =& $this->_addMixedPart(); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $text AND $attachments: + $message =& $this->_addMixedPart(); + $this->_addTextPart($message, $this->_txtbody); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html AND !$attachments AND !$html_images: + if (isset($this->_txtbody)) { + $message =& $this->_addAlternativePart($null); + $this->_addTextPart($message, $this->_txtbody); + $this->_addHtmlPart($message); + } else { + $message =& $this->_addHtmlPart($null); + } + break; + + case $html AND !$attachments AND $html_images: + $message =& $this->_addRelatedPart($null); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($message, $this->_html_images[$i]); + } + break; + + case $html AND $attachments AND !$html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html AND $attachments AND $html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $rel =& $this->_addRelatedPart($alt); + } else { + $rel =& $this->_addRelatedPart($message); + } + $this->_addHtmlPart($rel); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($rel, $this->_html_images[$i]); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + } + + if (isset($message)) { + $output = $message->encode(); + + $this->_headers = array_merge($this->_headers, + $output['headers']); + $body = $output['body']; + return $body; + + } else { + $ret = false; + return $ret; + } + } + + /** + * Returns an array with the headers needed to prepend to the email + * (MIME-Version and Content-Type). Format of argument is: + * $array['header-name'] = 'header-value'; + * + * @param array $xtra_headers Assoc array with any extra headers. + * Optional. + * @param bool $overwrite Overwrite already existing headers. + * + * @return array Assoc array with the mime headers + * @access public + */ + function &headers($xtra_headers = null, $overwrite = false) + { + // Content-Type header should already be present, + // So just add mime version header + $headers['MIME-Version'] = '1.0'; + if (isset($xtra_headers)) { + $headers = array_merge($headers, $xtra_headers); + } + if ($overwrite) { + $this->_headers = array_merge($this->_headers, $headers); + } else { + $this->_headers = array_merge($headers, $this->_headers); + } + + $encodedHeaders = $this->_encodeHeaders($this->_headers); + return $encodedHeaders; + } + + /** + * Get the text version of the headers + * (usefull if you want to use the PHP mail() function) + * + * @param array $xtra_headers Assoc array with any extra headers. + * Optional. + * @param bool $overwrite Overwrite the existing heaers with new. + * + * @return string Plain text headers + * @access public + */ + function txtHeaders($xtra_headers = null, $overwrite = false) + { + $headers = $this->headers($xtra_headers, $overwrite); + + $ret = ''; + foreach ($headers as $key => $val) { + $ret .= "$key: $val" . MAIL_MIME_CRLF; + } + return $ret; + } + + /** + * Sets the Subject header + * + * @param string $subject String to set the subject to. + * + * @return void + * @access public + */ + function setSubject($subject) + { + $this->_headers['Subject'] = $subject; + } + + /** + * Set an email to the From (the sender) header + * + * @param string $email The email address to use + * + * @return void + * @access public + */ + function setFrom($email) + { + $this->_headers['From'] = $email; + } + + /** + * Add an email to the Cc (carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addCc($email) + { + if (isset($this->_headers['Cc'])) { + $this->_headers['Cc'] .= ", $email"; + } else { + $this->_headers['Cc'] = $email; + } + } + + /** + * Add an email to the Bcc (blank carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addBcc($email) + { + if (isset($this->_headers['Bcc'])) { + $this->_headers['Bcc'] .= ", $email"; + } else { + $this->_headers['Bcc'] = $email; + } + } + + /** + * Since the PHP send function requires you to specifiy + * recipients (To: header) separately from the other + * headers, the To: header is not properly encoded. + * To fix this, you can use this public method to + * encode your recipients before sending to the send + * function + * + * @param string $recipients A comma-delimited list of recipients + * + * @return string Encoded data + * @access public + */ + function encodeRecipients($recipients) + { + $input = array("To" => $recipients); + $retval = $this->_encodeHeaders($input); + return $retval["To"] ; + } + + /** + * Encodes a header as per RFC2047 + * + * @param array $input The header data to encode + * @param array $params Extra build parameters + * + * @return array Encoded data + * @access private + */ + function _encodeHeaders($input, $params = array()) + { + + $build_params = $this->_build_params; + while (list($key, $value) = each($params)) { + $build_params[$key] = $value; + } + //$hdr_name: Name of the heaer + //$hdr_value: Full line of header value. + //$hdr_value_out: The recombined $hdr_val-atoms, or the encoded string. + + $useIconv = true; + if (isset($build_params['ignore-iconv'])) { + $useIconv = !$build_params['ignore-iconv']; + } + foreach ($input as $hdr_name => $hdr_value) { + if (preg_match('#([\x80-\xFF]){1}#', $hdr_value)) { + if (function_exists('iconv_mime_encode') && $useIconv) { + $imePrefs = array(); + if ($build_params['head_encoding'] == 'base64') { + $imePrefs['scheme'] = 'B'; + } else { + $imePrefs['scheme'] = 'Q'; + } + $imePrefs['input-charset'] = $build_params['head_charset']; + $imePrefs['output-charset'] = $build_params['head_charset']; + $imePrefs['line-length'] = 74; + $imePrefs['line-break-chars'] = "\r\n"; //Specified in RFC2047 + + $hdr_value = iconv_mime_encode($hdr_name, $hdr_value, $imePrefs); + $hdr_value = preg_replace("#^{$hdr_name}\:\ #", "", $hdr_value); + } elseif ($build_params['head_encoding'] == 'base64') { + //Base64 encoding has been selected. + //Base64 encode the entire string + $hdr_value = base64_encode($hdr_value); + + //Generate the header using the specified params and dynamicly + //determine the maximum length of such strings. + //75 is the value specified in the RFC. The first -2 is there so + //the later regexp doesn't break any of the translated chars. + //The -2 on the first line-regexp is to compensate for the ": " + //between the header-name and the header value + $prefix = '=?' . $build_params['head_charset'] . '?B?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix) - 2; + $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; + + //We can cut base4 every 4 characters, so the real max + //we can get must be rounded down. + $maxLength = $maxLength - ($maxLength % 4); + $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); + + $cutpoint = $maxLength1stLine; + $hdr_value_out = $hdr_value; + $output = ""; + while ($hdr_value_out) { + //Split translated string at every $maxLength + $part = substr($hdr_value_out, 0, $cutpoint); + $hdr_value_out = substr($hdr_value_out, $cutpoint); + $cutpoint = $maxLength; + //RFC 2047 specifies that any split header should + //be seperated by a CRLF SPACE. + if ($output) { + $output .= "\r\n "; + } + $output .= $prefix . $part . $suffix; + } + $hdr_value = $output; + } else { + //quoted-printable encoding has been selected + + //Fix for Bug #10298, Ota Mares <om@viazenetti.de> + //Check if there is a double quote at beginning or end of + //the string to prevent that an open or closing quote gets + //ignored because it is encapsuled by an encoding pre/suffix. + //Remove the double quote and set the specific prefix or + //suffix variable so that we can concat the encoded string and + //the double quotes back together to get the intended string. + $quotePrefix = $quoteSuffix = ''; + if ($hdr_value{0} == '"') { + $hdr_value = substr($hdr_value, 1); + $quotePrefix = '"'; + } + if ($hdr_value{strlen($hdr_value)-1} == '"') { + $hdr_value = substr($hdr_value, 0, -1); + $quoteSuffix = '"'; + } + + //Generate the header using the specified params and dynamicly + //determine the maximum length of such strings. + //75 is the value specified in the RFC. The -2 is there so + //the later regexp doesn't break any of the translated chars. + //The -2 on the first line-regexp is to compensate for the ": " + //between the header-name and the header value + $prefix = '=?' . $build_params['head_charset'] . '?Q?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix) - 2 - 1; + $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; + $maxLength = $maxLength - 1; + + //Replace all special characters used by the encoder. + $search = array('=', '_', '?', ' '); + $replace = array('=3D', '=5F', '=3F', '_'); + $hdr_value = str_replace($search, $replace, $hdr_value); + + //Replace all extended characters (\x80-xFF) with their + //ASCII values. + $hdr_value = preg_replace('#([\x80-\xFF])#e', + '"=" . strtoupper(dechex(ord("\1")))', + $hdr_value); + + //This regexp will break QP-encoded text at every $maxLength + //but will not break any encoded letters. + $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; + $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; + //Fix for Bug #10298, Ota Mares <om@viazenetti.de> + //Concat the double quotes and encoded string together + $hdr_value = $quotePrefix . $hdr_value . $quoteSuffix; + + + $hdr_value_out = $hdr_value; + $realMax = $maxLength1stLine + strlen($prefix . $suffix); + if (strlen($hdr_value_out) >= $realMax) { + //Begin with the regexp for the first line. + $reg = $reg1st; + $output = ""; + while ($hdr_value_out) { + //Split translated string at every $maxLength + //But make sure not to break any translated chars. + $found = preg_match($reg, $hdr_value_out, $matches); + + //After this first line, we need to use a different + //regexp for the first line. + $reg = $reg2nd; + + //Save the found part and encapsulate it in the + //prefix & suffix. Then remove the part from the + //$hdr_value_out variable. + if ($found) { + $part = $matches[0]; + $len = strlen($matches[0]); + $hdr_value_out = substr($hdr_value_out, $len); + } else { + $part = $hdr_value_out; + $hdr_value_out = ""; + } + + //RFC 2047 specifies that any split header should + //be seperated by a CRLF SPACE + if ($output) { + $output .= "\r\n "; + } + $output .= $prefix . $part . $suffix; + } + $hdr_value_out = $output; + } else { + $hdr_value_out = $prefix . $hdr_value_out . $suffix; + } + $hdr_value = $hdr_value_out; + } + } + $input[$hdr_name] = $hdr_value; + } + return $input; + } + + /** + * Set the object's end-of-line and define the constant if applicable. + * + * @param string $eol End Of Line sequence + * + * @return void + * @access private + */ + function _setEOL($eol) + { + $this->_eol = $eol; + if (!defined('MAIL_MIME_CRLF')) { + define('MAIL_MIME_CRLF', $this->_eol, true); + } + } + + + +} // End of class diff --git a/include/pear/Mail/mimeDecode.php b/include/pear/Mail/mimeDecode.php new file mode 100644 index 0000000000000000000000000000000000000000..b7984d681e38a30e5e546d2846019b93b38f3c33 --- /dev/null +++ b/include/pear/Mail/mimeDecode.php @@ -0,0 +1,860 @@ +<?php +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return + * the structure. Returned structure is similar to + * that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author George Schlossnagle <george@omniti.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mimeDecode.php,v 1.48 2006/12/03 13:43:33 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ + +require_once 'PEAR.php'; + + +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return the structure. + * Returned structure is similar to that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author George Schlossnagle <george@omniti.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimeDecode extends PEAR +{ + /** + * The raw email to decode + * + * @var string + * @access private + */ + var $_input; + + /** + * The header part of the input + * + * @var string + * @access private + */ + var $_header; + + /** + * The body part of the input + * + * @var string + * @access private + */ + var $_body; + + /** + * If an error occurs, this is used to store the message + * + * @var string + * @access private + */ + var $_error; + + /** + * Flag to determine whether to include bodies in the + * returned object. + * + * @var boolean + * @access private + */ + var $_include_bodies; + + /** + * Flag to determine whether to decode bodies + * + * @var boolean + * @access private + */ + var $_decode_bodies; + + /** + * Flag to determine whether to decode headers + * + * @var boolean + * @access private + */ + var $_decode_headers; + + /** + * Constructor. + * + * Sets up the object, initialise the variables, and splits and + * stores the header and body of the input. + * + * @param string The input to decode + * @access public + */ + function Mail_mimeDecode($input) + { + list($header, $body) = $this->_splitBodyHeader($input); + + $this->_input = $input; + $this->_header = $header; + $this->_body = $body; + $this->_decode_bodies = false; + $this->_include_bodies = true; + } + + /* Return raw header...added 10/23/07 by kip. + * + */ + function getHeader() { + return $this->_header; + } + + + + + /** + * Begins the decoding process. If called statically + * it will create an object and call the decode() method + * of it. + * + * @param array An array of various parameters that determine + * various things: + * include_bodies - Whether to include the body in the returned + * object. + * decode_bodies - Whether to decode the bodies + * of the parts. (Transfer encoding) + * decode_headers - Whether to decode headers + * input - If called statically, this will be treated + * as the input + * @return object Decoded results + * @access public + */ + function decode($params = null) + { + // determine if this method has been called statically + $isStatic = !(isset($this) && get_class($this) == __CLASS__); + + // Have we been called statically? + // If so, create an object and pass details to that. + if ($isStatic AND isset($params['input'])) { + + $obj = new Mail_mimeDecode($params['input']); + $structure = $obj->decode($params); + + // Called statically but no input + } elseif ($isStatic) { + return PEAR::raiseError('Called statically and no input given'); + + // Called via an object + } else { + $this->_include_bodies = isset($params['include_bodies']) ? + $params['include_bodies'] : false; + $this->_decode_bodies = isset($params['decode_bodies']) ? + $params['decode_bodies'] : false; + $this->_decode_headers = isset($params['decode_headers']) ? + $params['decode_headers'] : false; + + $structure = $this->_decode($this->_header, $this->_body); + if ($structure === false) { + $structure = $this->raiseError($this->_error); + } + } + + return $structure; + } + + /** + * Performs the decoding. Decodes the body string passed to it + * If it finds certain content-types it will call itself in a + * recursive fashion + * + * @param string Header section + * @param string Body section + * @return object Results of decoding process + * @access private + */ + function _decode($headers, $body, $default_ctype = 'text/plain') + { + $return = new stdClass; + $return->headers = array(); + $headers = $this->_parseHeaders($headers); + + foreach ($headers as $value) { + if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); + $return->headers[strtolower($value['name'])][] = $value['value']; + + } elseif (isset($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])][] = $value['value']; + + } else { + $return->headers[strtolower($value['name'])] = $value['value']; + } + } + + reset($headers); + while (list($key, $value) = each($headers)) { + $headers[$key]['name'] = strtolower($headers[$key]['name']); + switch ($headers[$key]['name']) { + + case 'content-type': + $content_type = $this->_parseHeaderValue($headers[$key]['value']); + + if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { + $return->ctype_primary = $regs[1]; + $return->ctype_secondary = $regs[2]; + } + + if (isset($content_type['other'])) { + while (list($p_name, $p_value) = each($content_type['other'])) { + $return->ctype_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-disposition': + $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); + $return->disposition = $content_disposition['value']; + if (isset($content_disposition['other'])) { + while (list($p_name, $p_value) = each($content_disposition['other'])) { + $return->d_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-transfer-encoding': + $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); + break; + } + } + + if (isset($content_type)) { + switch (strtolower($content_type['value'])) { + case 'text/plain': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'text/html': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'multipart/parallel': + case 'multipart/appledouble': // Appledouble mail + case 'multipart/report': // RFC1892 + case 'multipart/signed': // PGP + case 'multipart/digest': + case 'multipart/alternative': + case 'multipart/related': + case 'multipart/mixed': + if(!isset($content_type['other']['boundary'])){ + $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; + return false; + } + + $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; + + $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); + for ($i = 0; $i < count($parts); $i++) { + list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); + $part = $this->_decode($part_header, $part_body, $default_ctype); + if($part === false) + $part = $this->raiseError($this->_error); + $return->parts[] = $part; + } + break; + + case 'message/rfc822': + $obj = new Mail_mimeDecode($body); + $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, + 'decode_bodies' => $this->_decode_bodies, + 'decode_headers' => $this->_decode_headers)); + unset($obj); + break; + + default: + if(!isset($content_transfer_encoding['value'])) + $content_transfer_encoding['value'] = '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null; + break; + } + + } else { + $ctype = explode('/', $default_ctype); + $return->ctype_primary = $ctype[0]; + $return->ctype_secondary = $ctype[1]; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; + } + + return $return; + } + + /** + * Given the output of the above function, this will return an + * array of references to the parts, indexed by mime number. + * + * @param object $structure The structure to go through + * @param string $mime_number Internal use only. + * @return array Mime numbers + */ + function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') + { + $return = array(); + if (!empty($structure->parts)) { + if ($mime_number != '') { + $structure->mime_id = $prepend . $mime_number; + $return[$prepend . $mime_number] = &$structure; + } + for ($i = 0; $i < count($structure->parts); $i++) { + + + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { + $prepend = $prepend . $mime_number . '.'; + $_mime_number = ''; + } else { + $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); + } + + $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); + foreach ($arr as $key => $val) { + $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; + } + } + } else { + if ($mime_number == '') { + $mime_number = '1'; + } + $structure->mime_id = $prepend . $mime_number; + $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; + } + + return $return; + } + + /** + * Given a string containing a header and body + * section, this function will split them (at the first + * blank line) and return them. + * + * @param string Input to split apart + * @return array Contains header and body section + * @access private + */ + function _splitBodyHeader($input) + { + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { + return array($match[1], $match[2]); + } + $this->_error = 'Could not split header and body'; + return false; + } + + /** + * Parse headers given in $input and return + * as assoc array. + * + * @param string Headers to parse + * @return array Contains parsed headers + * @access private + */ + function _parseHeaders($input) + { + + if ($input !== '') { + // Unfold the input + $input = preg_replace("/\r?\n/", "\r\n", $input); + $input = preg_replace("/\r\n(\t| )+/", ' ', $input); + $headers = explode("\r\n", trim($input)); + + foreach ($headers as $value) { + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); + $hdr_value = substr($value, $pos+1); + if($hdr_value[0] == ' ') + $hdr_value = substr($hdr_value, 1); + + $return[] = array( + 'name' => $hdr_name, + 'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value + ); + } + } else { + $return = array(); + } + + return $return; + } + + /** + * Function to parse a header value, + * extract first part, and any secondary + * parts (after ;) This function is not as + * robust as it could be. Eg. header comments + * in the wrong place will probably break it. + * + * @param string Header value to parse + * @return array Contains parsed result + * @access private + */ + function _parseHeaderValue($input) + { + + if (($pos = strpos($input, ';')) !== false) { + + $return['value'] = trim(substr($input, 0, $pos)); + $input = trim(substr($input, $pos+1)); + + if (strlen($input) > 0) { + + // This splits on a semi-colon, if there's no preceeding backslash + // Now works with quoted values; had to glue the \; breaks in PHP + // the regex is already bordering on incomprehensible + $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/'; + preg_match_all($splitRegex, $input, $matches); + $parameters = array(); + for ($i=0; $i<count($matches[0]); $i++) { + $param = $matches[0][$i]; + while (substr($param, -2) == '\;') { + $param .= $matches[0][++$i]; + } + $parameters[] = $param; + } + + for ($i = 0; $i < count($parameters); $i++) { + $param_name = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ "); + $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ "); + if ($param_value[0] == '"') { + $param_value = substr($param_value, 1, -1); + } + $return['other'][$param_name] = $param_value; + $return['other'][strtolower($param_name)] = $param_value; + } + } + } else { + $return['value'] = trim($input); + } + + return $return; + } + + /** + * This function splits the input based + * on the given boundary + * + * @param string Input to parse + * @return array Contains array of resulting mime parts + * @access private + */ + function _boundarySplit($input, $boundary) + { + $parts = array(); + + $bs_possible = substr($boundary, 2, -2); + $bs_check = '\"' . $bs_possible . '\"'; + + if ($boundary == $bs_check) { + $boundary = $bs_possible; + } + + $tmp = explode('--' . $boundary, $input); + + for ($i = 1; $i < count($tmp) - 1; $i++) { + $parts[] = $tmp[$i]; + } + + return $parts; + } + + /** + * Given a header, this function will decode it + * according to RFC2047. Probably not *exactly* + * conformant, but it does pass all the given + * examples (in RFC2047). + * + * @param string Input header value to decode + * @return string Decoded header value + * @access private + */ + function _decodeHeader($input) + { + // Remove white space between encoded-words + $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); + + // For each encoded-word... + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { + + $encoded = $matches[1]; + $charset = $matches[2]; + $encoding = $matches[3]; + $text = $matches[4]; + + switch (strtolower($encoding)) { + case 'b': + $text = base64_decode($text); + break; + + case 'q': + $text = str_replace('_', ' ', $text); + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); + foreach($matches[1] as $value) + $text = str_replace('='.$value, chr(hexdec($value)), $text); + break; + } + + $input = str_replace($encoded, $text, $input); + } + + return $input; + } + + /** + * Given a body string and an encoding type, + * this function will decode and return it. + * + * @param string Input body to decode + * @param string Encoding type to use. + * @return string Decoded body + * @access private + */ + function _decodeBody($input, $encoding = '7bit') + { + switch (strtolower($encoding)) { + case '7bit': + return $input; + break; + + case 'quoted-printable': + return $this->_quotedPrintableDecode($input); + break; + + case 'base64': + return base64_decode($input); + break; + + default: + return $input; + } + } + + /** + * Given a quoted-printable string, this + * function will decode and return it. + * + * @param string Input body to decode + * @return string Decoded body + * @access private + */ + function _quotedPrintableDecode($input) + { + // Remove soft line breaks + $input = preg_replace("/=\r?\n/", '', $input); + + // Replace encoded characters + $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); + + return $input; + } + + /** + * Checks the input for uuencoded files and returns + * an array of them. Can be called statically, eg: + * + * $files =& Mail_mimeDecode::uudecode($some_text); + * + * It will check for the begin 666 ... end syntax + * however and won't just blindly decode whatever you + * pass it. + * + * @param string Input body to look for attahcments in + * @return array Decoded bodies, filenames and permissions + * @access public + * @author Unknown + */ + function &uudecode($input) + { + // Find all uuencoded sections + preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); + + for ($j = 0; $j < count($matches[3]); $j++) { + + $str = $matches[3][$j]; + $filename = $matches[2][$j]; + $fileperm = $matches[1][$j]; + + $file = ''; + $str = preg_split("/\r?\n/", trim($str)); + $strlen = count($str); + + for ($i = 0; $i < $strlen; $i++) { + $pos = 1; + $d = 0; + $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); + + while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); + + $pos += 4; + $d += 3; + } + + if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $pos += 3; + $d += 2; + } + + if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + } + } + $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); + } + + return $files; + } + + /** + * getSendArray() returns the arguments required for Mail::send() + * used to build the arguments for a mail::send() call + * + * Usage: + * $mailtext = Full email (for example generated by a template) + * $decoder = new Mail_mimeDecode($mailtext); + * $parts = $decoder->getSendArray(); + * if (!PEAR::isError($parts) { + * list($recipents,$headers,$body) = $parts; + * $mail = Mail::factory('smtp'); + * $mail->send($recipents,$headers,$body); + * } else { + * echo $parts->message; + * } + * @return mixed array of recipeint, headers,body or Pear_Error + * @access public + * @author Alan Knowles <alan@akbkhome.com> + */ + function getSendArray() + { + // prevent warning if this is not set + $this->_decode_headers = FALSE; + $headerlist =$this->_parseHeaders($this->_header); + $to = ""; + if (!$headerlist) { + return $this->raiseError("Message did not contain headers"); + } + foreach($headerlist as $item) { + $header[$item['name']] = $item['value']; + switch (strtolower($item['name'])) { + case "to": + case "cc": + case "bcc": + $to = ",".$item['value']; + default: + break; + } + } + if ($to == "") { + return $this->raiseError("Message did not contain any recipents"); + } + $to = substr($to,1); + return array($to,$header,$this->_body); + } + + /** + * Returns a xml copy of the output of + * Mail_mimeDecode::decode. Pass the output in as the + * argument. This function can be called statically. Eg: + * + * $output = $obj->decode(); + * $xml = Mail_mimeDecode::getXML($output); + * + * The DTD used for this should have been in the package. Or + * alternatively you can get it from cvs, or here: + * http://www.phpguru.org/xmail/xmail.dtd. + * + * @param object Input to convert to xml. This should be the + * output of the Mail_mimeDecode::decode function + * @return string XML version of input + * @access public + */ + function getXML($input) + { + $crlf = "\r\n"; + $output = '<?xml version=\'1.0\' ?>' . $crlf . + '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf . + '<email>' . $crlf . + Mail_mimeDecode::_getXML($input) . + '</email>'; + + return $output; + } + + /** + * Function that does the actual conversion to xml. Does a single + * mimepart at a time. + * + * @param object Input to convert to xml. This is a mimepart object. + * It may or may not contain subparts. + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML($input, $indent = 1) + { + $htab = "\t"; + $crlf = "\r\n"; + $output = ''; + $headers = @(array)$input->headers; + + foreach ($headers as $hdr_name => $hdr_value) { + + // Multiple headers with this name + if (is_array($headers[$hdr_name])) { + for ($i = 0; $i < count($hdr_value); $i++) { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); + } + + // Only one header of this sort + } else { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); + } + } + + if (!empty($input->parts)) { + for ($i = 0; $i < count($input->parts); $i++) { + $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . + str_repeat($htab, $indent) . '</mimepart>' . $crlf; + } + } elseif (isset($input->body)) { + $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' . + $input->body . ']]></body>' . $crlf; + } + + return $output; + } + + /** + * Helper function to _getXML(). Returns xml of a header. + * + * @param string Name of header + * @param string Value of header + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML_helper($hdr_name, $hdr_value, $indent) + { + $htab = "\t"; + $crlf = "\r\n"; + $return = ''; + + $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); + $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); + + // Sort out any parameters + if (!empty($new_hdr_value['other'])) { + foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { + $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf . + str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf; + } + + $params = implode('', $params); + } else { + $params = ''; + } + + $return = str_repeat($htab, $indent) . '<header>' . $crlf . + str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf . + str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf . + $params . + str_repeat($htab, $indent) . '</header>' . $crlf; + + return $return; + } + +} // End of class diff --git a/include/pear/Mail/mimePart.php b/include/pear/Mail/mimePart.php new file mode 100644 index 0000000000000000000000000000000000000000..7ad54e5ef58d1c6ee919b310af05dbfa38bf7a8b --- /dev/null +++ b/include/pear/Mail/mimePart.php @@ -0,0 +1,439 @@ +<?php +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id: mimePart.php,v 1.25 2007/05/14 21:43:08 cipri Exp $ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimePart { + + /** + * The encoding type of this part + * + * @var string + * @access private + */ + var $_encoding; + + /** + * An array of subparts + * + * @var array + * @access private + */ + var $_subparts; + + /** + * The output of this part after being built + * + * @var string + * @access private + */ + var $_encoded; + + /** + * Headers for this part + * + * @var array + * @access private + */ + var $_headers; + + /** + * The body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body; + + /** + * Constructor. + * + * Sets up the object. + * + * @param $body - The body of the mime part if any. + * @param $params - An associative array of parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, base64, or quoted-printable + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * dfilename - Optional filename parameter for content disposition + * description - Content description + * charset - Character set to use + * @access public + */ + function Mail_mimePart($body = '', $params = array()) + { + if (!defined('MAIL_MIMEPART_CRLF')) { + define('MAIL_MIMEPART_CRLF', defined('MAIL_MIME_CRLF') ? MAIL_MIME_CRLF : "\r\n", TRUE); + } + + $contentType = array(); + $contentDisp = array(); + foreach ($params as $key => $value) { + switch ($key) { + case 'content_type': + $contentType['type'] = $value; + //$headers['Content-Type'] = $value . (isset($charset) ? '; charset="' . $charset . '"' : ''); + break; + + case 'encoding': + $this->_encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'disposition': + $contentDisp['disp'] = $value; + break; + + case 'dfilename': + $contentDisp['filename'] = $value; + $contentType['name'] = $value; + break; + + case 'description': + $headers['Content-Description'] = $value; + break; + + case 'charset': + $contentType['charset'] = $value; + $contentDisp['charset'] = $value; + break; + + case 'language': + $contentType['language'] = $value; + $contentDisp['language'] = $value; + break; + + case 'location': + $headers['Content-Location'] = $value; + break; + + } + } + if (isset($contentType['type'])) { + $headers['Content-Type'] = $contentType['type']; + if (isset($contentType['name'])) { + $headers['Content-Type'] .= ';' . MAIL_MIMEPART_CRLF; + $headers['Content-Type'] .= $this->_buildHeaderParam('name', $contentType['name'], + isset($contentType['charset']) ? $contentType['charset'] : 'US-ASCII', + isset($contentType['language']) ? $contentType['language'] : NULL); + } elseif (isset($contentType['charset'])) { + $headers['Content-Type'] .= "; charset=\"{$contentType['charset']}\""; + } + } + + + if (isset($contentDisp['disp'])) { + $headers['Content-Disposition'] = $contentDisp['disp']; + if (isset($contentDisp['filename'])) { + $headers['Content-Disposition'] .= ';' . MAIL_MIMEPART_CRLF; + $headers['Content-Disposition'] .= $this->_buildHeaderParam('filename', $contentDisp['filename'], + isset($contentDisp['charset']) ? $contentDisp['charset'] : 'US-ASCII', + isset($contentDisp['language']) ? $contentDisp['language'] : NULL); + } + } + + + + + // Default content-type + if (!isset($headers['Content-Type'])) { + $headers['Content-Type'] = 'text/plain'; + } + + //Default encoding + if (!isset($this->_encoding)) { + $this->_encoding = '7bit'; + } + + // Assign stuff to member variables + $this->_encoded = array(); + $this->_headers = $headers; + $this->_body = $body; + } + + /** + * encode() + * + * Encodes and returns the email. Also stores + * it in the encoded member variable + * + * @return An associative array containing two elements, + * body and headers. The headers element is itself + * an indexed array. + * @access public + */ + function encode() + { + $encoded =& $this->_encoded; + + if (count($this->_subparts)) { + srand((double)microtime()*1000000); + $boundary = '=_' . md5(rand() . microtime()); + $this->_headers['Content-Type'] .= ';' . MAIL_MIMEPART_CRLF . "\t" . 'boundary="' . $boundary . '"'; + + // Add body parts to $subparts + for ($i = 0; $i < count($this->_subparts); $i++) { + $headers = array(); + $tmp = $this->_subparts[$i]->encode(); + foreach ($tmp['headers'] as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $subparts[] = implode(MAIL_MIMEPART_CRLF, $headers) . MAIL_MIMEPART_CRLF . MAIL_MIMEPART_CRLF . $tmp['body'] . MAIL_MIMEPART_CRLF; + } + + $encoded['body'] = '--' . $boundary . MAIL_MIMEPART_CRLF . + rtrim(implode('--' . $boundary . MAIL_MIMEPART_CRLF , $subparts), MAIL_MIMEPART_CRLF) . MAIL_MIMEPART_CRLF . + '--' . $boundary.'--' . MAIL_MIMEPART_CRLF; + + } else { + $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); + } + + // Add headers to $encoded + $encoded['headers'] =& $this->_headers; + + return $encoded; + } + + /** + * &addSubPart() + * + * Adds a subpart to current mime part and returns + * a reference to it + * + * @param $body The body of the subpart, if any. + * @param $params The parameters for the subpart, same + * as the $params argument for constructor. + * @return A reference to the part you just added. It is + * crucial if using multipart/* in your subparts that + * you use =& in your script when calling this function, + * otherwise you will not be able to add further subparts. + * @access public + */ + function &addSubPart($body, $params) + { + $this->_subparts[] = new Mail_mimePart($body, $params); + return $this->_subparts[count($this->_subparts) - 1]; + } + + /** + * _getEncodedData() + * + * Returns encoded data based upon encoding passed to it + * + * @param $data The data to encode. + * @param $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * @access private + */ + function _getEncodedData($data, $encoding) + { + switch ($encoding) { + case '8bit': + case '7bit': + return $data; + break; + + case 'quoted-printable': + return $this->_quotedPrintableEncode($data); + break; + + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, MAIL_MIMEPART_CRLF)); + break; + + default: + return $data; + } + } + + /** + * quotedPrintableEncode() + * + * Encodes data to quoted-printable standard. + * + * @param $input The data to encode + * @param $line_max Optional max line length. Should + * not be more than 76 chars + * + * @access private + */ + function _quotedPrintableEncode($input , $line_max = 76) + { + $lines = preg_split("/\r?\n/", $input); + $eol = MAIL_MIMEPART_CRLF; + $escape = '='; + $output = ''; + + while (list(, $line) = each($lines)) { + + $line = preg_split('||', $line, -1, PREG_SPLIT_NO_EMPTY); + $linlen = count($line); + $newline = ''; + + for ($i = 0; $i < $linlen; $i++) { + $char = $line[$i]; + $dec = ord($char); + + if (($dec == 32) AND ($i == ($linlen - 1))) { // convert space at eol only + $char = '=20'; + + } elseif (($dec == 9) AND ($i == ($linlen - 1))) { // convert tab at eol only + $char = '=09'; + } elseif ($dec == 9) { + ; // Do nothing if a tab. + } elseif (($dec == 61) OR ($dec < 32 ) OR ($dec > 126)) { + $char = $escape . strtoupper(sprintf('%02s', dechex($dec))); + } elseif (($dec == 46) AND ($newline == '')) { + //Bug #9722: convert full-stop at bol + //Some Windows servers need this, won't break anything (cipri) + $char = '=2E'; + } + + if ((strlen($newline) + strlen($char)) >= $line_max) { // MAIL_MIMEPART_CRLF is not counted + $output .= $newline . $escape . $eol; // soft line break; " =\r\n" is okay + $newline = ''; + } + $newline .= $char; + } // end of for + $output .= $newline . $eol; + } + $output = substr($output, 0, -1 * strlen($eol)); // Don't want last crlf + return $output; + } + + /** + * _buildHeaderParam() + * + * Encodes the paramater of a header. + * + * @param $name The name of the header-parameter + * @param $value The value of the paramter + * @param $charset The characterset of $value + * @param $language The language used in $value + * @param $maxLength The maximum length of a line. Defauls to 75 + * + * @access private + */ + function _buildHeaderParam($name, $value, $charset=NULL, $language=NULL, $maxLength=75) + { + //If we find chars to encode, or if charset or language + //is not any of the defaults, we need to encode the value. + $shouldEncode = 0; + $secondAsterisk = ''; + if (preg_match('#([\x80-\xFF]){1}#', $value)) { + $shouldEncode = 1; + } elseif ($charset && (strtolower($charset) != 'us-ascii')) { + $shouldEncode = 1; + } elseif ($language && ($language != 'en' && $language != 'en-us')) { + $shouldEncode = 1; + } + if ($shouldEncode) { + $search = array('%', ' ', "\t"); + $replace = array('%25', '%20', '%09'); + $encValue = str_replace($search, $replace, $value); + $encValue = preg_replace('#([\x80-\xFF])#e', '"%" . strtoupper(dechex(ord("\1")))', $encValue); + $value = "$charset'$language'$encValue"; + $secondAsterisk = '*'; + } + $header = " {$name}{$secondAsterisk}=\"{$value}\"; "; + if (strlen($header) <= $maxLength) { + return $header; + } + + $preLength = strlen(" {$name}*0{$secondAsterisk}=\""); + $sufLength = strlen("\";"); + $maxLength = MAX(16, $maxLength - $preLength - $sufLength - 2); + $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; + + $headers = array(); + $headCount = 0; + while ($value) { + $matches = array(); + $found = preg_match($maxLengthReg, $value, $matches); + if ($found) { + $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$matches[0]}\""; + $value = substr($value, strlen($matches[0])); + } else { + $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$value}\""; + $value = ""; + } + $headCount++; + } + $headers = implode(MAIL_MIMEPART_CRLF, $headers) . ';'; + return $headers; + } +} // End of class diff --git a/include/pear/Mail/mock.php b/include/pear/Mail/mock.php new file mode 100644 index 0000000000000000000000000000000000000000..1dfdadb8bb39e47a0b76446505e0de2e3234210c --- /dev/null +++ b/include/pear/Mail/mock.php @@ -0,0 +1,119 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: mock.php,v 1.1 2007/12/08 17:57:54 chagenbu Exp $ +// + +/** + * Mock implementation of the PEAR Mail:: interface for testing. + * @access public + * @package Mail + * @version $Revision: 1.1 $ + */ +class Mail_mock extends Mail { + + /** + * Array of messages that have been sent with the mock. + * + * @var array + * @access public + */ + var $sentMessages = array(); + + /** + * Callback before sending mail. + * + * @var callback + */ + var $_preSendCallback; + + /** + * Callback after sending mai. + * + * @var callback + */ + var $_postSendCallback; + + /** + * Constructor. + * + * Instantiates a new Mail_mock:: object based on the parameters + * passed in. It looks for the following parameters, both optional: + * preSendCallback Called before an email would be sent. + * postSendCallback Called after an email would have been sent. + * + * @param array Hash containing any parameters. + * @access public + */ + function Mail_mock($params) + { + if (isset($params['preSendCallback']) && + is_callable($params['preSendCallback'])) { + $this->_preSendCallback = $params['preSendCallback']; + } + + if (isset($params['postSendCallback']) && + is_callable($params['postSendCallback'])) { + $this->_postSendCallback = $params['postSendCallback']; + } + } + + /** + * Implements Mail_mock::send() function. Silently discards all + * mail. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + if ($this->_preSendCallback) { + call_user_func_array($this->_preSendCallback, + array(&$this, $recipients, $headers, $body)); + } + + $entry = array('recipients' => $recipients, 'headers' => $headers, 'body' => $body); + $this->sentMessages[] = $entry; + + if ($this->_postSendCallback) { + call_user_func_array($this->_postSendCallback, + array(&$this, $recipients, $headers, $body)); + } + + return true; + } + +} diff --git a/include/pear/Mail/null.php b/include/pear/Mail/null.php new file mode 100644 index 0000000000000000000000000000000000000000..982bfa45b6d652132a5219fb273d4327e505e856 --- /dev/null +++ b/include/pear/Mail/null.php @@ -0,0 +1,60 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Phil Kernick <philk@rotfl.com.au> | +// +----------------------------------------------------------------------+ +// +// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $ +// + +/** + * Null implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.2 $ + */ +class Mail_null extends Mail { + + /** + * Implements Mail_null::send() function. Silently discards all + * mail. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + return true; + } + +} diff --git a/include/pear/Mail/sendmail.php b/include/pear/Mail/sendmail.php new file mode 100644 index 0000000000000000000000000000000000000000..183890a80990851b7e11c6bac6a939a2e603bbf3 --- /dev/null +++ b/include/pear/Mail/sendmail.php @@ -0,0 +1,171 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ + +/** + * Sendmail implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 1.20 $ + */ +class Mail_sendmail extends Mail { + + /** + * The location of the sendmail or sendmail wrapper binary on the + * filesystem. + * @var string + */ + var $sendmail_path = '/usr/sbin/sendmail'; + + /** + * Any extra command-line parameters to pass to the sendmail or + * sendmail wrapper binary. + * @var string + */ + var $sendmail_args = '-i'; + + /** + * Constructor. + * + * Instantiates a new Mail_sendmail:: object based on the parameters + * passed in. It looks for the following parameters: + * sendmail_path The location of the sendmail binary on the + * filesystem. Defaults to '/usr/sbin/sendmail'. + * + * sendmail_args Any extra parameters to pass to the sendmail + * or sendmail wrapper binary. + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array $params Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_sendmail($params) + { + if (isset($params['sendmail_path'])) { + $this->sendmail_path = $params['sendmail_path']; + } + if (isset($params['sendmail_args'])) { + $this->sendmail_args = $params['sendmail_args']; + } + + /* + * Because we need to pass message headers to the sendmail program on + * the commandline, we can't guarantee the use of the standard "\r\n" + * separator. Instead, we use the system's native line separator. + */ + if (defined('PHP_EOL')) { + $this->sep = PHP_EOL; + } else { + $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; + } + } + + /** + * Implements Mail::send() function using the sendmail + * command-line binary. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + return $recipients; + } + $recipients = escapeShellCmd(implode(' ', $recipients)); + + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list($from, $text_headers) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + return PEAR::raiseError('No from address given.'); + } elseif (strpos($from, ' ') !== false || + strpos($from, ';') !== false || + strpos($from, '&') !== false || + strpos($from, '`') !== false) { + return PEAR::raiseError('From address specified with dangerous characters.'); + } + + $from = escapeshellarg($from); // Security bug #16200 + + $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); + if (!$mail) { + return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); + } + + // Write the headers following by two newlines: one to end the headers + // section and a second to separate the headers block from the body. + fputs($mail, $text_headers . $this->sep . $this->sep); + + fputs($mail, $body); + $result = pclose($mail); + if (version_compare(phpversion(), '4.2.3') == -1) { + // With older php versions, we need to shift the pclose + // result to get the exit code. + $result = $result >> 8 & 0xFF; + } + + if ($result != 0) { + return PEAR::raiseError('sendmail returned error code ' . $result, + $result); + } + + return true; + } + +} diff --git a/include/pear/Mail/smtp.php b/include/pear/Mail/smtp.php new file mode 100644 index 0000000000000000000000000000000000000000..850695d39fce73df99b98a373358b4214167a086 --- /dev/null +++ b/include/pear/Mail/smtp.php @@ -0,0 +1,421 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Chuck Hagenbuch <chuck@horde.org> | +// | Jon Parise <jon@php.net> | +// +----------------------------------------------------------------------+ + +/** Error: Failed to create a Net_SMTP object */ +define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); + +/** Error: Failed to connect to SMTP server */ +define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); + +/** Error: SMTP authentication failure */ +define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); + +/** Error: No From: address has been provided */ +define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); + +/** Error: Failed to set sender */ +define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); + +/** Error: Failed to add recipient */ +define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); + +/** Error: Failed to send data */ +define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); + +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * @access public + * @package Mail + * @version $Revision: 1.33 $ + */ +class Mail_smtp extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The list of service extension parameters to pass to the Net_SMTP + * mailFrom() command. + * @var array + */ + var $_extparams = array(); + + /** + * The SMTP host to connect to. + * @var string + */ + var $host = 'localhost'; + + /** + * The port the SMTP server is on. + * @var integer + */ + var $port = 25; + + /** + * Should SMTP authentication be used? + * + * This value may be set to true, false or the name of a specific + * authentication method. + * + * If the value is set to true, the Net_SMTP package will attempt to use + * the best authentication method advertised by the remote SMTP server. + * + * @var mixed + */ + var $auth = false; + + /** + * The username to use if the SMTP server requires authentication. + * @var string + */ + var $username = ''; + + /** + * The password to use if the SMTP server requires authentication. + * @var string + */ + var $password = ''; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + */ + var $localhost = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = null; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $debug + */ + var $debug = false; + + /** + * Indicates whether or not the SMTP connection should persist over + * multiple calls to the send() method. + * + * @var boolean + */ + var $persist = false; + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server + * supports it. This speeds up delivery over high-latency connections. By + * default, use the default value supplied by Net_SMTP. + * @var bool + */ + var $pipelining; + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * host The server to connect to. Defaults to localhost. + * port The port to connect to. Defaults to 25. + * auth SMTP authentication. Defaults to none. + * username The username to use for SMTP auth. No default. + * password The password to use for SMTP auth. No default. + * localhost The local hostname / domain. Defaults to localhost. + * timeout The SMTP connection timeout. Defaults to none. + * verp Whether to use VERP or not. Defaults to false. + * DEPRECATED as of 1.2.0 (use setMailParams()). + * debug Activate SMTP debug mode? Defaults to false. + * persist Should the SMTP connection persist? + * pipelining Use SMTP command pipelining + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @param array Hash containing any parameters different from the + * defaults. + * @access public + */ + function Mail_smtp($params) + { + if (isset($params['host'])) $this->host = $params['host']; + if (isset($params['port'])) $this->port = $params['port']; + if (isset($params['auth'])) $this->auth = $params['auth']; + if (isset($params['username'])) $this->username = $params['username']; + if (isset($params['password'])) $this->password = $params['password']; + if (isset($params['localhost'])) $this->localhost = $params['localhost']; + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['debug'])) $this->debug = (bool)$params['debug']; + if (isset($params['persist'])) $this->persist = (bool)$params['persist']; + if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining']; + + // Deprecated options + if (isset($params['verp'])) { + $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']); + } + + register_shutdown_function(array(&$this, '_Mail_smtp')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function _Mail_smtp() + { + $this->disconnect(); + } + + /** + * Implements Mail::send() function using SMTP. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (e.g., 'Subject'), and the array value + * is the header value (e.g., 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * MIME parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * @access public + */ + function send($recipients, $headers, $body) + { + /* If we don't already have an SMTP object, create one. */ + $result = &$this->getSMTPObject(); + if (PEAR::isError($result)) { + return $result; + } + + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $this->_sanitizeHeaders($headers); + + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + $this->_smtp->rset(); + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + /* Since few MTAs are going to allow this header to be forged + * unless it's in the MAIL FROM: exchange, we'll use + * Return-Path instead of From: if it's set. */ + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + + if (!isset($from)) { + $this->_smtp->rset(); + return PEAR::raiseError('No From: address has been provided', + PEAR_MAIL_SMTP_ERROR_FROM); + } + + $params = null; + if (!empty($this->_extparams)) { + foreach ($this->_extparams as $key => $val) { + $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val); + } + } + if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) { + $error = $this->_error("Failed to set sender: $from", $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); + } + + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + $this->_smtp->rset(); + return $recipients; + } + + foreach ($recipients as $recipient) { + $res = $this->_smtp->rcptTo($recipient); + if (is_a($res, 'PEAR_Error')) { + $error = $this->_error("Failed to add recipient: $recipient", $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); + } + } + + /* Send the message's headers and the body as SMTP data. */ + $res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body); + if (is_a($res, 'PEAR_Error')) { + $error = $this->_error('Failed to send data', $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); + } + + /* If persistent connections are disabled, destroy our SMTP object. */ + if ($this->persist === false) { + $this->disconnect(); + } + + return true; + } + + /** + * Connection wrapper + * + * + */ + function &connect() { + return $this->getSMTPObject(); + } + + + /** + * Connect to the SMTP server by instantiating a Net_SMTP object. + * + * @return mixed Returns a reference to the Net_SMTP object on success, or + * a PEAR_Error containing a descriptive error message on + * failure. + * + * @since 1.2.0 + * @access public + */ + function &getSMTPObject() + { + if (is_object($this->_smtp) !== false) { + return $this->_smtp; + } + + include_once 'Net/SMTP.php'; + $this->_smtp = &new Net_SMTP($this->host, + $this->port, + $this->localhost); + + /* If we still don't have an SMTP object at this point, fail. */ + if (is_object($this->_smtp) === false) { + return PEAR::raiseError('Failed to create a Net_SMTP object', + PEAR_MAIL_SMTP_ERROR_CREATE); + } + + /* Configure the SMTP connection. */ + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + /* Attempt to connect to the configured SMTP server. */ + if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { + + $error = $this->_error('Failed to connect to ' . + $this->host . ':' . $this->port, + $res); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); + } + + /* Attempt to authenticate if authentication has been enabled. */ + if ($this->auth) { + $method = is_string($this->auth) ? $this->auth : ''; + + if (PEAR::isError($res = $this->_smtp->auth($this->username, + $this->password, + $method))) { + $error = $this->_error("$method authentication failure", + $res); + $this->_smtp->rset(); + return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); + } + } + + return $this->_smtp; + } + + /** + * Add parameter associated with a SMTP service extension. + * + * @param string Extension keyword. + * @param string Any value the keyword needs. + * + * @since 1.2.0 + * @access public + */ + function addServiceExtensionParameter($keyword, $value = null) + { + $this->_extparams[$keyword] = $value; + } + + + + + /** + * Disconnect and destroy the current SMTP connection. + * + * @return boolean True if the SMTP connection no longer exists. + * + * @since 1.1.9 + * @access public + */ + function disconnect() + { + /* If we have an SMTP object, disconnect and destroy it. */ + if (is_object($this->_smtp) && $this->_smtp->disconnect()) { + $this->_smtp = null; + } + + /* We are disconnected if we no longer have an SMTP object. */ + return ($this->_smtp === null); + } + + /** + * Build a standardized string describing the current SMTP error. + * + * @param string $text Custom string describing the error context. + * @param object $error Reference to the current PEAR_Error object. + * + * @return string A string describing the current SMTP error. + * + * @since 1.1.7 + * @access private + */ + function _error($text, &$error) + { + /* Split the SMTP response into a code and a response string. */ + list($code, $response) = $this->_smtp->getResponse(); + + /* Build our standardized error string. */ + return $text + . ' [SMTP: ' . $error->getMessage() + . " (code: $code, response: $response)]"; + } + +} diff --git a/include/pear/Net/SMTP.php b/include/pear/Net/SMTP.php new file mode 100644 index 0000000000000000000000000000000000000000..bf0a5a1aa0440a2e6ce1dc0ceb392832e0ab14b8 --- /dev/null +++ b/include/pear/Net/SMTP.php @@ -0,0 +1,1080 @@ +<?php +/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Chuck Hagenbuch <chuck@horde.org> | +// | Jon Parise <jon@php.net> | +// | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | +// +----------------------------------------------------------------------+ +// +// $Id: SMTP.php,v 1.64 2008/12/20 23:03:49 jon Exp $ + +require_once 'PEAR.php'; +require_once 'Net/Socket.php'; + +/** + * Provides an implementation of the SMTP protocol using PEAR's + * Net_Socket:: class. + * + * @package Net_SMTP + * @author Chuck Hagenbuch <chuck@horde.org> + * @author Jon Parise <jon@php.net> + * @author Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> + * + * @example basic.php A basic implementation of the Net_SMTP package. + */ +class Net_SMTP +{ + /** + * The server to connect to. + * @var string + * @access public + */ + var $host = 'localhost'; + + /** + * The port to connect to. + * @var int + * @access public + */ + var $port = 25; + + /** + * The value to give when sending EHLO or HELO. + * @var string + * @access public + */ + var $localhost = 'localhost'; + + /** + * List of supported authentication methods, in preferential order. + * @var array + * @access public + */ + var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP + * server supports it. + * + * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), + * somlFrom() and samlFrom() do not wait for a response from the + * SMTP server but return immediately. + * + * @var bool + * @access public + */ + var $pipelining = false; + + /** + * Number of pipelined commands. + * @var int + * @access private + */ + var $_pipelined_commands = 0; + + /** + * Should debugging output be enabled? + * @var boolean + * @access private + */ + var $_debug = false; + + /** + * The socket resource being used to connect to the SMTP server. + * @var resource + * @access private + */ + var $_socket = null; + + /** + * The most recent server response code. + * @var int + * @access private + */ + var $_code = -1; + + /** + * The most recent server response arguments. + * @var array + * @access private + */ + var $_arguments = array(); + + /** + * Stores detected features of the SMTP server. + * @var array + * @access private + */ + var $_esmtp = array(); + + /** + * Instantiates a new Net_SMTP object, overriding any defaults + * with parameters that are passed in. + * + * If you have SSL support in PHP, you can connect to a server + * over SSL using an 'ssl://' prefix: + * + * // 465 is a common smtps port. + * $smtp = new Net_SMTP('ssl://mail.host.com', 465); + * $smtp->connect(); + * + * @param string $host The server to connect to. + * @param integer $port The port to connect to. + * @param string $localhost The value to give when sending EHLO or HELO. + * @param boolean $pipeling Use SMTP command pipelining + * + * @access public + * @since 1.0 + */ + function Net_SMTP($host = null, $port = null, $localhost = null, $pipelining = false) + { + if (isset($host)) { + $this->host = $host; + } + if (isset($port)) { + $this->port = $port; + } + if (isset($localhost)) { + $this->localhost = $localhost; + } + $this->pipelining = $pipelining; + + $this->_socket = new Net_Socket(); + + /* Include the Auth_SASL package. If the package is not + * available, we disable the authentication methods that + * depend upon it. */ + if ((@include_once 'Auth/SASL.php') === false) { + $pos = array_search('DIGEST-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + $pos = array_search('CRAM-MD5', $this->auth_methods); + unset($this->auth_methods[$pos]); + } + } + + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + * + * @access public + * @since 1.1.0 + */ + function setDebug($debug) + { + $this->_debug = $debug; + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access private + * @since 1.1.0 + */ + function _send($data) + { + if ($this->_debug) { + echo "DEBUG: Send: $data\n"; + } + + if (PEAR::isError($error = $this->_socket->write($data))) { + return PEAR::raiseError('Failed to write to socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Send a command to the server with an optional string of + * arguments. A carriage return / linefeed (CRLF) sequence will + * be appended to each command string before it is sent to the + * SMTP server - an error will be thrown if the command string + * already contains any newline characters. Use _send() for + * commands that must contain newlines. + * + * @param string $command The SMTP command to send to the server. + * @param string $args A string of optional arguments to append + * to the command. + * + * @return mixed The result of the _send() call. + * + * @access private + * @since 1.1.0 + */ + function _put($command, $args = '') + { + if (!empty($args)) { + $command .= ' ' . $args; + } + + if (strcspn($command, "\r\n") !== strlen($command)) { + return PEAR::raiseError('Commands cannot contain newlines'); + } + + return $this->_send($command . "\r\n"); + } + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * @param bool $later Do not parse the response now, but wait + * until the last command in the pipelined + * command group + * + * @return mixed True if the server returned a valid response code or + * a PEAR_Error object is an error condition is reached. + * + * @access private + * @since 1.1.0 + * + * @see getResponse + */ + function _parseResponse($valid, $later = false) + { + $this->_code = -1; + $this->_arguments = array(); + + + if ($later) { + $this->_pipelined_commands++; + return true; + } + + for ($i = 0; $i <= $this->_pipelined_commands; $i++) { + while ($line = $this->_socket->readLine()) { + if ($this->_debug) { + echo "DEBUG: Recv: $line\n"; + } + + /* If we receive an empty line, the connection has been closed. */ + if (empty($line)) { + $this->disconnect(); + return PEAR::raiseError('Connection was unexpectedly closed'); + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + } + + $this->_pipelined_commands = 0; + + /* Compare the server's response code with the valid code/codes. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } elseif (is_array($valid) && in_array($this->_code, $valid, true)) { + return true; + } + + return PEAR::raiseError('Invalid response code received from server', + $this->_code); + } + + /** + * Return a 2-tuple containing the last response from the SMTP server. + * + * @return array A two-element array: the first element contains the + * response code as an integer and the second element + * contains the response's arguments as a string. + * + * @access public + * @since 1.1.0 + */ + function getResponse() + { + return array($this->_code, join("\n", $this->_arguments)); + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection. + * @param bool $persistent Should a persistent socket connection + * be used? + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function connect($timeout = null, $persistent = false) + { + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout); + + if (PEAR::isError($result)) { + return PEAR::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + + if (PEAR::isError($error = $this->_parseResponse(220))) { + return $error; + } + if (PEAR::isError($error = $this->_negotiate())) { + return $error; + } + + + return true; + } + + /** + * Attempt to disconnect from the SMTP server. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function disconnect() + { + if (PEAR::isError($error = $this->_put('QUIT'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(221))) { + return $error; + } + if (PEAR::isError($error = $this->_socket->disconnect())) { + return PEAR::raiseError('Failed to disconnect socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Attempt to send the EHLO command and obtain a list of ESMTP + * extensions available, and failing that just send HELO. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access private + * @since 1.1.0 + */ + function _negotiate() + { + if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) { + return $error; + } + + if (PEAR::isError($this->_parseResponse(250))) { + /* If we receive a 503 response, we're already authenticated. */ + if ($this->_code === 503) { + return true; + } + + /* If the EHLO failed, try the simpler HELO command. */ + if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) { + return $error; + } + if (PEAR::isError($this->_parseResponse(250))) { + return PEAR::raiseError('HELO was not accepted: ', $this->_code); + } + + return true; + } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + if (!isset($this->_esmtp['PIPELINING'])) { + $this->pipelining = false; + } + + return true; + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @return mixed Returns a string containing the name of the best + * supported authentication method or a PEAR_Error object + * if a failure condition is encountered. + * @access private + * @since 1.1.0 + */ + function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + + foreach ($this->auth_methods as $method) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return PEAR::raiseError('No supported authentication methods'); + } + + /** + * Attempt to do SMTP authentication. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The requested authentication method. If none is + * specified, the best supported method will be used. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function auth($uid, $pwd , $method = '') + { + if (version_compare(PHP_VERSION, '5.1.0', '>=') && (isset($this->_esmtp['STARTTLS']) || ($this->_esmtp['STARTTLS'] == true))) { + if (PEAR::isError($result = $this->_put('STARTTLS'))) { + return $result; + } + if (PEAR::isError($result = $this->_parseResponse(220))) { + return $result; + } + if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { + return $result; + } elseif ($result !== true) { + return PEAR::raiseError('STARTTLS failed'); + } + + /* Send EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + } + + if (empty($this->_esmtp['AUTH'])) { + return PEAR::raiseError('SMTP server does not support authentication'); + } + + /* If no method has been specified, get the name of the best + * supported method advertised by the SMTP server. */ + if (empty($method)) { + if (PEAR::isError($method = $this->_getBestAuthMethod())) { + /* Return the PEAR_Error object from _getBestAuthMethod(). */ + return $method; + } + } else { + $method = strtoupper($method); + if (!in_array($method, $this->auth_methods)) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + } + + switch ($method) { + case 'DIGEST-MD5': + $result = $this->_authDigest_MD5($uid, $pwd); + break; + + case 'CRAM-MD5': + $result = $this->_authCRAM_MD5($uid, $pwd); + break; + + case 'LOGIN': + $result = $this->_authLogin($uid, $pwd); + break; + + case 'PLAIN': + $result = $this->_authPlain($uid, $pwd); + break; + + default: + $result = PEAR::raiseError("$method is not a supported authentication method"); + break; + } + + /* If an error was encountered, return the PEAR_Error object. */ + if (PEAR::isError($result)) { + return $result; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authDigest_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $digest = &Auth_SASL::factory('digestmd5'); + $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, + $this->host, "smtp")); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + /* We don't use the protocol's third step because SMTP doesn't + * allow subsequent authentication, so we just silently ignore + * it. */ + if (PEAR::isError($error = $this->_put(''))) { + return $error; + } + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authCRAM_MD5($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $cram = &Auth_SASL::factory('crammd5'); + $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authLogin($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($uid)))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authPlain($uid, $pwd) + { + if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Send the HELO command. + * + * @param string The domain name to say we are. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function helo($domain) + { + if (PEAR::isError($error = $this->_put('HELO', $domain))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Return the list of SMTP service extensions advertised by the server. + * + * @return array The list of SMTP service extensions. + * @access public + * @since 1.3 + */ + function getServiceExtensions() + { + return $this->_esmtp; + } + + /** + * Send the MAIL FROM: command. + * + * @param string $sender The sender (reverse path) to set. + * @param string $params String containing additional MAIL parameters, + * such as the NOTIFY flags defined by RFC 1891 + * or the VERP protocol. + * + * If $params is an array, only the 'verp' option + * is supported. If 'verp' is true, the XVERP + * parameter is appended to the MAIL command. If + * the 'verp' value is a string, the full + * XVERP=value parameter is appended. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function mailFrom($sender, $params = null) + { + $args = "FROM:<$sender>"; + + /* Support the deprecated array form of $params. */ + if (is_array($params) && isset($params['verp'])) { + /* XVERP */ + if ($params['verp'] === true) { + $args .= ' XVERP'; + + /* XVERP=something */ + } elseif (trim($params['verp'])) { + $args .= ' XVERP=' . $params['verp']; + } + } elseif (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('MAIL', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the RCPT TO: command. + * + * @param string $recipient The recipient (forward path) to add. + * @param string $params String containing additional RCPT parameters, + * such as the NOTIFY flags defined by RFC 1891. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + */ + function rcptTo($recipient, $params = null) + { + $args = "TO:<$recipient>"; + if (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('RCPT', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Quote the data so that it meets SMTP standards. + * + * This is provided as a separate public function to facilitate + * easier overloading for the cases where it is desirable to + * customize the quoting behavior. + * + * @param string $data The message text to quote. The string must be passed + * by reference, and the text will be modified in place. + * + * @access public + * @since 1.2 + */ + function quotedata(&$data) + { + /* Change Unix (\n) and Mac (\r) linefeeds into + * Internet-standard CRLF (\r\n) linefeeds. */ + $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data); + + /* Because a single leading period (.) signifies an end to the + * data, legitimate leading periods need to be "doubled" + * (e.g. '..'). */ + $data = str_replace("\n.", "\n..", $data); + } + + /** + * Send the DATA command. + * + * @param string $data The message body to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function data($data) + { + /* RFC 1870, section 3, subsection 3 states "a value of zero + * indicates that no fixed maximum message size is in force". + * Furthermore, it says that if "the parameter is omitted no + * information is conveyed about the server's fixed maximum + * message size". */ + if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { + if (strlen($data) >= $this->_esmtp['SIZE']) { + $this->disconnect(); + return PEAR::raiseError('Message size excedes the server limit'); + } + } + + /* Quote the data based on the SMTP standards. */ + $this->quotedata($data); + + if (PEAR::isError($error = $this->_put('DATA'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(354))) { + return $error; + } + + if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { + return $result; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the SEND FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function sendFrom($path) + { + if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for sendFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function send_from($path) + { + return sendFrom($path); + } + + /** + * Send the SOML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function somlFrom($path) + { + if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for somlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function soml_from($path) + { + return somlFrom($path); + } + + /** + * Send the SAML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function samlFrom($path) + { + if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for samlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function saml_from($path) + { + return samlFrom($path); + } + + /** + * Send the RSET command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function rset() + { + if (PEAR::isError($error = $this->_put('RSET'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the VRFY command. + * + * @param string The string to verify + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function vrfy($string) + { + /* Note: 251 is also a valid response code */ + if (PEAR::isError($error = $this->_put('VRFY', $string))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) { + return $error; + } + + return true; + } + + /** + * Send the NOOP command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function noop() + { + if (PEAR::isError($error = $this->_put('NOOP'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility method. identifySender()'s functionality is + * now handled internally. + * + * @return boolean This method always return true. + * + * @access public + * @since 1.0 + */ + function identifySender() + { + return true; + } + +} diff --git a/include/pear/Net/Socket.php b/include/pear/Net/Socket.php new file mode 100644 index 0000000000000000000000000000000000000000..cc495bf9fc621c41c5c30bdef83c06bf1c74670e --- /dev/null +++ b/include/pear/Net/Socket.php @@ -0,0 +1,593 @@ +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.0 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Stig Bakken <ssb@php.net> | +// | Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ +// +// $Id: Socket.php,v 1.38 2008/02/15 18:24:17 chagenbu Exp $ + +require_once 'PEAR.php'; + +define('NET_SOCKET_READ', 1); +define('NET_SOCKET_WRITE', 2); +define('NET_SOCKET_ERROR', 4); + +/** + * Generalized Socket class. + * + * @version 1.1 + * @author Stig Bakken <ssb@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + */ +class Net_Socket extends PEAR { + + /** + * Socket file pointer. + * @var resource $fp + */ + var $fp = null; + + /** + * Whether the socket is blocking. Defaults to true. + * @var boolean $blocking + */ + var $blocking = true; + + /** + * Whether the socket is persistent. Defaults to false. + * @var boolean $persistent + */ + var $persistent = false; + + /** + * The IP address to connect to. + * @var string $addr + */ + var $addr = ''; + + /** + * The port number to connect to. + * @var integer $port + */ + var $port = 0; + + /** + * Number of seconds to wait on socket connections before assuming + * there's no more data. Defaults to no timeout. + * @var integer $timeout + */ + var $timeout = false; + + /** + * Number of bytes to read at a time in readLine() and + * readAll(). Defaults to 2048. + * @var integer $lineLength + */ + var $lineLength = 2048; + + /** + * Connect to the specified port. If called when the socket is + * already connected, it disconnects and connects again. + * + * @param string $addr IP address or host name. + * @param integer $port TCP port number. + * @param boolean $persistent (optional) Whether the connection is + * persistent (kept open between requests + * by the web server). + * @param integer $timeout (optional) How long to wait for data. + * @param array $options See options for stream_context_create. + * + * @access public + * + * @return boolean | PEAR_Error True on success or a PEAR_Error on failure. + */ + function connect($addr, $port = 0, $persistent = null, $timeout = null, $options = null) + { + if (is_resource($this->fp)) { + @fclose($this->fp); + $this->fp = null; + } + + if (!$addr) { + return $this->raiseError('$addr cannot be empty'); + } elseif (strspn($addr, '.0123456789') == strlen($addr) || + strstr($addr, '/') !== false) { + $this->addr = $addr; + } else { + $this->addr = @gethostbyname($addr); + } + + $this->port = $port % 65536; + + if ($persistent !== null) { + $this->persistent = $persistent; + } + + if ($timeout !== null) { + $this->timeout = $timeout; + } + + $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen'; + $errno = 0; + $errstr = ''; + $old_track_errors = @ini_set('track_errors', 1); + if ($options && function_exists('stream_context_create')) { + if ($this->timeout) { + $timeout = $this->timeout; + } else { + $timeout = 0; + } + $context = stream_context_create($options); + + // Since PHP 5 fsockopen doesn't allow context specification + if (function_exists('stream_socket_client')) { + $flags = $this->persistent ? STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT; + $addr = $this->addr . ':' . $this->port; + $fp = stream_socket_client($addr, $errno, $errstr, $timeout, $flags, $context); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout, $context); + } + } else { + if ($this->timeout) { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $this->timeout); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr); + } + } + + if (!$fp) { + if ($errno == 0 && isset($php_errormsg)) { + $errstr = $php_errormsg; + } + @ini_set('track_errors', $old_track_errors); + return $this->raiseError($errstr, $errno); + } + + + @ini_set('track_errors', $old_track_errors); + $this->fp = $fp; + + return $this->setBlocking($this->blocking); + } + + /** + * Disconnects from the peer, closes the socket. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function disconnect() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + @fclose($this->fp); + $this->fp = null; + return true; + } + + /** + * Find out if the socket is in blocking mode. + * + * @access public + * @return boolean The current blocking mode. + */ + function isBlocking() + { + return $this->blocking; + } + + /** + * Sets whether the socket connection should be blocking or + * not. A read call to a non-blocking socket will return immediately + * if there is no data available, whereas it will block until there + * is data for blocking sockets. + * + * @param boolean $mode True for blocking sockets, false for nonblocking. + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function setBlocking($mode) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + $this->blocking = $mode; + socket_set_blocking($this->fp, $this->blocking); + + return true; + } + + /** + * Sets the timeout value on socket descriptor, + * expressed in the sum of seconds and microseconds + * + * @param integer $seconds Seconds. + * @param integer $microseconds Microseconds. + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function setTimeout($seconds, $microseconds) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_set_timeout($this->fp, $seconds, $microseconds); + } + + /** + * Sets the file buffering size on the stream. + * See php's stream_set_write_buffer for more information. + * + * @param integer $size Write buffer size. + * @access public + * @return mixed on success or an PEAR_Error object otherwise + */ + function setWriteBuffer($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $returned = stream_set_write_buffer($this->fp, $size); + if ($returned == 0) { + return true; + } + return $this->raiseError('Cannot set write buffer.'); + } + + /** + * Returns information about an existing socket resource. + * Currently returns four entries in the result array: + * + * <p> + * timed_out (bool) - The socket timed out waiting for data<br> + * blocked (bool) - The socket was blocked<br> + * eof (bool) - Indicates EOF event<br> + * unread_bytes (int) - Number of bytes left in the socket buffer<br> + * </p> + * + * @access public + * @return mixed Array containing information about existing socket resource or a PEAR_Error instance otherwise + */ + function getStatus() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_get_status($this->fp); + } + + /** + * Get a specified line of data + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function gets($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fgets($this->fp, $size); + } + + /** + * Read a specified amount of data. This is guaranteed to return, + * and has the added benefit of getting everything in one fread() + * chunk; if you know the size of the data you're getting + * beforehand, this is definitely the way to go. + * + * @param integer $size The number of bytes to read from the socket. + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function read($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fread($this->fp, $size); + } + + /** + * Write a specified amount of data. + * + * @param string $data Data to write. + * @param integer $blocksize Amount of data to write at once. + * NULL means all at once. + * + * @access public + * @return mixed If the socket is not connected, returns an instance of PEAR_Error + * If the write succeeds, returns the number of bytes written + * If the write fails, returns false. + */ + function write($data, $blocksize = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($blocksize) && !OS_WINDOWS) { + return @fwrite($this->fp, $data); + } else { + if (is_null($blocksize)) { + $blocksize = 1024; + } + + $pos = 0; + $size = strlen($data); + while ($pos < $size) { + $written = @fwrite($this->fp, substr($data, $pos, $blocksize)); + if ($written === false) { + return false; + } + $pos += $written; + } + + return $pos; + } + } + + /** + * Write a line of data to the socket, followed by a trailing "\r\n". + * + * @access public + * @return mixed fputs result, or an error + */ + function writeLine($data) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return fwrite($this->fp, $data . "\r\n"); + } + + /** + * Tests for end-of-file on a socket descriptor. + * + * Also returns true if the socket is disconnected. + * + * @access public + * @return bool + */ + function eof() + { + return (!is_resource($this->fp) || feof($this->fp)); + } + + /** + * Reads a byte of data + * + * @access public + * @return 1 byte of data from the socket, or a PEAR_Error if + * not connected. + */ + function readByte() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return ord(@fread($this->fp, 1)); + } + + /** + * Reads a word of data + * + * @access public + * @return 1 word of data from the socket, or a PEAR_Error if + * not connected. + */ + function readWord() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 2); + return (ord($buf[0]) + (ord($buf[1]) << 8)); + } + + /** + * Reads an int of data + * + * @access public + * @return integer 1 int of data from the socket, or a PEAR_Error if + * not connected. + */ + function readInt() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return (ord($buf[0]) + (ord($buf[1]) << 8) + + (ord($buf[2]) << 16) + (ord($buf[3]) << 24)); + } + + /** + * Reads a zero-terminated string of data + * + * @access public + * @return string, or a PEAR_Error if + * not connected. + */ + function readString() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $string = ''; + while (($char = @fread($this->fp, 1)) != "\x00") { + $string .= $char; + } + return $string; + } + + /** + * Reads an IP Address and returns it in a dot formatted string + * + * @access public + * @return Dot formatted string, or a PEAR_Error if + * not connected. + */ + function readIPAddress() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return sprintf('%d.%d.%d.%d', ord($buf[0]), ord($buf[1]), + ord($buf[2]), ord($buf[3])); + } + + /** + * Read until either the end of the socket or a newline, whichever + * comes first. Strips the trailing newline from the returned data. + * + * @access public + * @return All available data up to a newline, without that + * newline, or until the end of the socket, or a PEAR_Error if + * not connected. + */ + function readLine() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $line = ''; + $timeout = time() + $this->timeout; + while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) { + $line .= @fgets($this->fp, $this->lineLength); + if (substr($line, -1) == "\n") { + return rtrim($line, "\r\n"); + } + } + return $line; + } + + /** + * Read until the socket closes, or until there is no more data in + * the inner PHP buffer. If the inner buffer is empty, in blocking + * mode we wait for at least 1 byte of data. Therefore, in + * blocking mode, if there is no data at all to be read, this + * function will never exit (unless the socket is closed on the + * remote end). + * + * @access public + * + * @return string All data until the socket closes, or a PEAR_Error if + * not connected. + */ + function readAll() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $data = ''; + while (!feof($this->fp)) { + $data .= @fread($this->fp, $this->lineLength); + } + return $data; + } + + /** + * Runs the equivalent of the select() system call on the socket + * with a timeout specified by tv_sec and tv_usec. + * + * @param integer $state Which of read/write/error to check for. + * @param integer $tv_sec Number of seconds for timeout. + * @param integer $tv_usec Number of microseconds for timeout. + * + * @access public + * @return False if select fails, integer describing which of read/write/error + * are ready, or PEAR_Error if not connected. + */ + function select($state, $tv_sec, $tv_usec = 0) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $read = null; + $write = null; + $except = null; + if ($state & NET_SOCKET_READ) { + $read[] = $this->fp; + } + if ($state & NET_SOCKET_WRITE) { + $write[] = $this->fp; + } + if ($state & NET_SOCKET_ERROR) { + $except[] = $this->fp; + } + if (false === ($sr = stream_select($read, $write, $except, $tv_sec, $tv_usec))) { + return false; + } + + $result = 0; + if (count($read)) { + $result |= NET_SOCKET_READ; + } + if (count($write)) { + $result |= NET_SOCKET_WRITE; + } + if (count($except)) { + $result |= NET_SOCKET_ERROR; + } + return $result; + } + + /** + * Turns encryption on/off on a connected socket. + * + * @param bool $enabled Set this parameter to true to enable encryption + * and false to disable encryption. + * @param integer $type Type of encryption. See + * http://se.php.net/manual/en/function.stream-socket-enable-crypto.php for values. + * + * @access public + * @return false on error, true on success and 0 if there isn't enough data and the + * user should try again (non-blocking sockets only). A PEAR_Error object + * is returned if the socket is not connected + */ + function enableCrypto($enabled, $type) + { + if (version_compare(phpversion(), "5.1.0", ">=")) { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + return @stream_socket_enable_crypto($this->fp, $enabled, $type); + } else { + return $this->raiseError('Net_Socket::enableCrypto() requires php version >= 5.1.0'); + } + } + +} diff --git a/include/pear/PEAR.php b/include/pear/PEAR.php new file mode 100644 index 0000000000000000000000000000000000000000..4de89ee3ace62cf172d259cee0733c8b6e6b11d5 --- /dev/null +++ b/include/pear/PEAR.php @@ -0,0 +1,1129 @@ +<?php +/** + * PEAR, the PHP Extension and Application Repository + * + * PEAR class and PEAR_Error class + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Sterling Hughes <sterling@php.net> + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V.Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: PEAR.php,v 1.112 2009/04/15 04:05:13 dufuz Exp $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/**#@+ + * ERROR constants + */ +define('PEAR_ERROR_RETURN', 1); +define('PEAR_ERROR_PRINT', 2); +define('PEAR_ERROR_TRIGGER', 4); +define('PEAR_ERROR_DIE', 8); +define('PEAR_ERROR_CALLBACK', 16); +/** + * WARNING: obsolete + * @deprecated + */ +define('PEAR_ERROR_EXCEPTION', 32); +/**#@-*/ +define('PEAR_ZE2', (function_exists('version_compare') && + version_compare(zend_version(), "2-dev", "ge"))); + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); + define('OS_UNIX', false); + define('PEAR_OS', 'Windows'); +} else { + define('OS_WINDOWS', false); + define('OS_UNIX', true); + define('PEAR_OS', 'Unix'); // blatant assumption +} + +$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; +$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; +$GLOBALS['_PEAR_destructor_object_list'] = array(); +$GLOBALS['_PEAR_shutdown_funcs'] = array(); +$GLOBALS['_PEAR_error_handler_stack'] = array(); + +@ini_set('track_errors', true); + +/** + * Base class for other PEAR classes. Provides rudimentary + * emulation of destructors. + * + * If you want a destructor in your class, inherit PEAR and make a + * destructor method called _yourclassname (same name as the + * constructor, but with a "_" prefix). Also, in your constructor you + * have to call the PEAR constructor: $this->PEAR();. + * The destructor method will be called without parameters. Note that + * at in some SAPI implementations (such as Apache), any output during + * the request shutdown (in which destructors are called) seems to be + * discarded. If you need to get any debug information from your + * destructor, use error_log(), syslog() or something similar. + * + * IMPORTANT! To use the emulated destructors you need to create the + * objects by reference: $obj =& new PEAR_child; + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.8.1 + * @link http://pear.php.net/package/PEAR + * @see PEAR_Error + * @since Class available since PHP 4.0.2 + * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear + */ +class PEAR +{ + // {{{ properties + + /** + * Whether to enable internal debug messages. + * + * @var bool + * @access private + */ + var $_debug = false; + + /** + * Default error mode for this object. + * + * @var int + * @access private + */ + var $_default_error_mode = null; + + /** + * Default error options used for this object when error mode + * is PEAR_ERROR_TRIGGER. + * + * @var int + * @access private + */ + var $_default_error_options = null; + + /** + * Default error handler (callback) for this object, if error mode is + * PEAR_ERROR_CALLBACK. + * + * @var string + * @access private + */ + var $_default_error_handler = ''; + + /** + * Which class to use for error objects. + * + * @var string + * @access private + */ + var $_error_class = 'PEAR_Error'; + + /** + * An array of expected errors. + * + * @var array + * @access private + */ + var $_expected_errors = array(); + + // }}} + + // {{{ constructor + + /** + * Constructor. Registers this object in + * $_PEAR_destructor_object_list for destructor emulation if a + * destructor object exists. + * + * @param string $error_class (optional) which class to use for + * error objects, defaults to PEAR_Error. + * @access public + * @return void + */ + function PEAR($error_class = null) + { + $classname = strtolower(get_class($this)); + if ($this->_debug) { + print "PEAR constructor called, class=$classname\n"; + } + if ($error_class !== null) { + $this->_error_class = $error_class; + } + while ($classname && strcasecmp($classname, "pear")) { + $destructor = "_$classname"; + if (method_exists($this, $destructor)) { + global $_PEAR_destructor_object_list; + $_PEAR_destructor_object_list[] = &$this; + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + break; + } else { + $classname = get_parent_class($classname); + } + } + } + + // }}} + // {{{ destructor + + /** + * Destructor (the emulated type of...). Does nothing right now, + * but is included for forward compatibility, so subclass + * destructors should always call it. + * + * See the note in the class desciption about output from + * destructors. + * + * @access public + * @return void + */ + function _PEAR() { + if ($this->_debug) { + printf("PEAR destructor called, class=%s\n", strtolower(get_class($this))); + } + } + + // }}} + // {{{ getStaticProperty() + + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + + return $properties[$class][$var]; + } + + // }}} + // {{{ registerShutdownFunc() + + /** + * Use this function to register a shutdown method for static + * classes. + * + * @access public + * @param mixed $func The function name (or array of class/method) to call + * @param mixed $args The arguments to pass to the function + * @return void + */ + function registerShutdownFunc($func, $args = array()) + { + // if we are called statically, there is a potential + // that no shutdown func is registered. Bug #6445 + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); + } + + // }}} + // {{{ isError() + + /** + * Tell whether a value is a PEAR error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true + * only if $code is a string and + * $obj->getMessage() == $code or + * $code is an integer and $obj->getCode() == $code + * @access public + * @return bool true if parameter is an error + */ + function isError($data, $code = null) + { + if (!is_a($data, 'PEAR_Error')) { + return false; + } + + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } + + return $data->getCode() == $code; + } + + // }}} + // {{{ setErrorHandling() + + /** + * Sets how errors generated by this object should be handled. + * Can be invoked both in objects and statically. If called + * statically, setErrorHandling sets the default behaviour for all + * PEAR objects. If called in an object, setErrorHandling sets + * the default behaviour for that object. + * + * @param int $mode + * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION. + * + * @param mixed $options + * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one + * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * + * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected + * to be the callback function or method. A callback + * function is a string with the name of the function, a + * callback method is an array of two elements: the element + * at index 0 is the object, and the element at index 1 is + * the name of the method to call in the object. + * + * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is + * a printf format string used when printing the error + * message. + * + * @access public + * @return void + * @see PEAR_ERROR_RETURN + * @see PEAR_ERROR_PRINT + * @see PEAR_ERROR_TRIGGER + * @see PEAR_ERROR_DIE + * @see PEAR_ERROR_CALLBACK + * @see PEAR_ERROR_EXCEPTION + * + * @since PHP 4.0.5 + */ + + function setErrorHandling($mode = null, $options = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $setmode = &$this->_default_error_mode; + $setoptions = &$this->_default_error_options; + } else { + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + } + + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + } + + // }}} + // {{{ expectError() + + /** + * This method is used to tell which errors you expect to get. + * Expected errors are always returned with error mode + * PEAR_ERROR_RETURN. Expected error codes are stored in a stack, + * and this method pushes a new element onto it. The list of + * expected errors are in effect until they are popped off the + * stack with the popExpect() method. + * + * Note that this method can not be called statically + * + * @param mixed $code a single error code or an array of error codes to expect + * + * @return int the new depth of the "expected errors" stack + * @access public + */ + function expectError($code = '*') + { + if (is_array($code)) { + array_push($this->_expected_errors, $code); + } else { + array_push($this->_expected_errors, array($code)); + } + return sizeof($this->_expected_errors); + } + + // }}} + // {{{ popExpect() + + /** + * This method pops one element off the expected error codes + * stack. + * + * @return array the list of error codes that were popped + */ + function popExpect() + { + return array_pop($this->_expected_errors); + } + + // }}} + // {{{ _checkDelExpect() + + /** + * This method checks unsets an error code if available + * + * @param mixed error code + * @return bool true if the error code was unset, false otherwise + * @access private + * @since PHP 4.3.0 + */ + function _checkDelExpect($error_code) + { + $deleted = false; + + foreach ($this->_expected_errors AS $key => $error_array) { + if (in_array($error_code, $error_array)) { + unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); + $deleted = true; + } + + // clean up empty arrays + if (0 == count($this->_expected_errors[$key])) { + unset($this->_expected_errors[$key]); + } + } + return $deleted; + } + + // }}} + // {{{ delExpect() + + /** + * This method deletes all occurences of the specified element from + * the expected error codes stack. + * + * @param mixed $error_code error code that should be deleted + * @return mixed list of error codes that were deleted or error + * @access public + * @since PHP 4.3.0 + */ + function delExpect($error_code) + { + $deleted = false; + if ((is_array($error_code) && (0 != count($error_code)))) { + // $error_code is a non-empty array here; + // we walk through it trying to unset all + // values + foreach($error_code as $key => $error) { + if ($this->_checkDelExpect($error)) { + $deleted = true; + } else { + $deleted = false; + } + } + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } elseif (!empty($error_code)) { + // $error_code comes alone, trying to unset it + if ($this->_checkDelExpect($error_code)) { + return true; + } else { + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + } + + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } + + // }}} + // {{{ raiseError() + + /** + * This method is a wrapper that returns an instance of the + * configured error class with this object's default error + * handling applied. If the $mode and $options parameters are not + * specified, the object's defaults are used. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION. + * + * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter + * specifies the PHP-internal error level (one of + * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * If $mode is PEAR_ERROR_CALLBACK, this + * parameter specifies the callback function or + * method. In other error modes this parameter + * is ignored. + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @param string $error_class The returned error object will be + * instantiated from this class, if specified. + * + * @param bool $skipmsg If true, raiseError will only pass error codes, + * the error message parameter will be dropped. + * + * @access public + * @return object a PEAR error object + * @see PEAR::setErrorHandling + * @since PHP 4.0.5 + */ + function &raiseError($message = null, + $code = null, + $mode = null, + $options = null, + $userinfo = null, + $error_class = null, + $skipmsg = false) + { + // The error is yet a PEAR error object + if (is_object($message)) { + $code = $message->getCode(); + $userinfo = $message->getUserInfo(); + $error_class = $message->getType(); + $message->error_message_prefix = ''; + $message = $message->getMessage(); + } + + if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ($exp[0] == "*" || + (is_int(reset($exp)) && in_array($code, $exp)) || + (is_string(reset($exp)) && in_array($message, $exp))) { + $mode = PEAR_ERROR_RETURN; + } + } + + // No mode given, try global ones + if ($mode === null) { + // Class error handler + if (isset($this) && isset($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + // Global error handler + } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) { + $mode = $GLOBALS['_PEAR_default_error_mode']; + $options = $GLOBALS['_PEAR_default_error_options']; + } + } + + if ($error_class !== null) { + $ec = $error_class; + } elseif (isset($this) && isset($this->_error_class)) { + $ec = $this->_error_class; + } else { + $ec = 'PEAR_Error'; + } + + if (intval(PHP_VERSION) < 5) { + // little non-eval hack to fix bug #12147 + include 'PEAR/FixPHP5PEARWarnings.php'; + return $a; + } + + if ($skipmsg) { + $a = new $ec($code, $mode, $options, $userinfo); + } else { + $a = new $ec($message, $code, $mode, $options, $userinfo); + } + + return $a; + } + + // }}} + // {{{ throwError() + + /** + * Simpler form of raiseError with fewer options. In most cases + * message, code and userinfo are enough. + * + * @param string $message + * + */ + function &throwError($message = null, + $code = null, + $userinfo = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $a = &$this->raiseError($message, $code, null, null, $userinfo); + return $a; + } + + $a = &PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; + } + + // }}} + function staticPushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + $stack[] = array($def_mode, $def_options); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $def_mode = $mode; + $def_options = $options; + break; + + case PEAR_ERROR_CALLBACK: + $def_mode = $mode; + // class/object method callback + if (is_callable($options)) { + $def_options = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + $stack[] = array($mode, $options); + return true; + } + + function staticPopErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + return true; + } + + // {{{ pushErrorHandling() + + /** + * Push a new error handler on top of the error handler options stack. With this + * you can easily override the actual error handler for some code and restore + * it later with popErrorHandling. + * + * @param mixed $mode (same as setErrorHandling) + * @param mixed $options (same as setErrorHandling) + * + * @return bool Always true + * + * @see PEAR::setErrorHandling + */ + function pushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + if (isset($this) && is_a($this, 'PEAR')) { + $def_mode = &$this->_default_error_mode; + $def_options = &$this->_default_error_options; + } else { + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + } + $stack[] = array($def_mode, $def_options); + + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + $stack[] = array($mode, $options); + return true; + } + + // }}} + // {{{ popErrorHandling() + + /** + * Pop the last error handler used + * + * @return bool Always true + * + * @see PEAR::pushErrorHandling + */ + function popErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + return true; + } + + // }}} + // {{{ loadExtension() + + /** + * OS independant PHP extension load. Remember to take care + * on the correct extension name for case sensitive OSes. + * + * @param string $ext The extension name + * @return bool Success or not on the dl() call + */ + function loadExtension($ext) + { + if (!extension_loaded($ext)) { + // if either returns true dl() will produce a FATAL error, stop that + if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { + return false; + } + + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } + + return true; + } + + // }}} +} + +if (PEAR_ZE2) { + include_once 'PEAR5.php'; +} + +// {{{ _PEAR_call_destructors() + +function _PEAR_call_destructors() +{ + global $_PEAR_destructor_object_list; + if (is_array($_PEAR_destructor_object_list) && + sizeof($_PEAR_destructor_object_list)) + { + reset($_PEAR_destructor_object_list); + if (PEAR_ZE2) { + $destructLifoExists = PEAR5::getStaticProperty('PEAR', 'destructlifo'); + } else { + $destructLifoExists = PEAR::getStaticProperty('PEAR', 'destructlifo'); + } + + if ($destructLifoExists) { + $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); + } + + while (list($k, $objref) = each($_PEAR_destructor_object_list)) { + $classname = get_class($objref); + while ($classname) { + $destructor = "_$classname"; + if (method_exists($objref, $destructor)) { + $objref->$destructor(); + break; + } else { + $classname = get_parent_class($classname); + } + } + } + // Empty the object list to ensure that destructors are + // not called more than once. + $_PEAR_destructor_object_list = array(); + } + + // Now call the shutdown functions + if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { + call_user_func_array($value[0], $value[1]); + } + } +} + +// }}} +/** + * Standard PEAR error class for PHP 4 + * + * This class is supserseded by {@link PEAR_Exception} in PHP 5 + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Gregory Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.8.1 + * @link http://pear.php.net/manual/en/core.pear.pear-error.php + * @see PEAR::raiseError(), PEAR::throwError() + * @since Class available since PHP 4.0.2 + */ +class PEAR_Error +{ + // {{{ properties + + var $error_message_prefix = ''; + var $mode = PEAR_ERROR_RETURN; + var $level = E_USER_NOTICE; + var $code = -1; + var $message = ''; + var $userinfo = ''; + var $backtrace = null; + + // }}} + // {{{ constructor + + /** + * PEAR_Error constructor + * + * @param string $message message + * + * @param int $code (optional) error code + * + * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN, + * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION + * + * @param mixed $options (optional) error level, _OR_ in the case of + * PEAR_ERROR_CALLBACK, the callback function or object/method + * tuple. + * + * @param string $userinfo (optional) additional user/debug info + * + * @access public + * + */ + function PEAR_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + if ($mode === null) { + $mode = PEAR_ERROR_RETURN; + } + $this->message = $message; + $this->code = $code; + $this->mode = $mode; + $this->userinfo = $userinfo; + + if (PEAR_ZE2) { + $skiptrace = PEAR5::getStaticProperty('PEAR_Error', 'skiptrace'); + } else { + $skiptrace = PEAR::getStaticProperty('PEAR_Error', 'skiptrace'); + } + + if (!$skiptrace) { + $this->backtrace = debug_backtrace(); + if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { + unset($this->backtrace[0]['object']); + } + } + if ($mode & PEAR_ERROR_CALLBACK) { + $this->level = E_USER_NOTICE; + $this->callback = $options; + } else { + if ($options === null) { + $options = E_USER_NOTICE; + } + $this->level = $options; + $this->callback = null; + } + if ($this->mode & PEAR_ERROR_PRINT) { + if (is_null($options) || is_int($options)) { + $format = "%s"; + } else { + $format = $options; + } + printf($format, $this->getMessage()); + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + trigger_error($this->getMessage(), $this->level); + } + if ($this->mode & PEAR_ERROR_DIE) { + $msg = $this->getMessage(); + if (is_null($options) || is_int($options)) { + $format = "%s"; + if (substr($msg, -1) != "\n") { + $msg .= "\n"; + } + } else { + $format = $options; + } + die(sprintf($format, $msg)); + } + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_callable($this->callback)) { + call_user_func($this->callback, $this); + } + } + if ($this->mode & PEAR_ERROR_EXCEPTION) { + trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); + eval('$e = new Exception($this->message, $this->code);throw($e);'); + } + } + + // }}} + // {{{ getMode() + + /** + * Get the error mode from an error object. + * + * @return int error mode + * @access public + */ + function getMode() { + return $this->mode; + } + + // }}} + // {{{ getCallback() + + /** + * Get the callback function/method from an error object. + * + * @return mixed callback function or object/method array + * @access public + */ + function getCallback() { + return $this->callback; + } + + // }}} + // {{{ getMessage() + + + /** + * Get the error message from an error object. + * + * @return string full error message + * @access public + */ + function getMessage() + { + return ($this->error_message_prefix . $this->message); + } + + + // }}} + // {{{ getCode() + + /** + * Get error code from an error object + * + * @return int error code + * @access public + */ + function getCode() + { + return $this->code; + } + + // }}} + // {{{ getType() + + /** + * Get the name of this error/exception. + * + * @return string error/exception name (type) + * @access public + */ + function getType() + { + return get_class($this); + } + + // }}} + // {{{ getUserInfo() + + /** + * Get additional user-supplied information. + * + * @return string user-supplied information + * @access public + */ + function getUserInfo() + { + return $this->userinfo; + } + + // }}} + // {{{ getDebugInfo() + + /** + * Get additional debug information supplied by the application. + * + * @return string debug information + * @access public + */ + function getDebugInfo() + { + return $this->getUserInfo(); + } + + // }}} + // {{{ getBacktrace() + + /** + * Get the call backtrace from where the error was generated. + * Supported with PHP 4.3.0 or newer. + * + * @param int $frame (optional) what frame to fetch + * @return array Backtrace, or NULL if not available. + * @access public + */ + function getBacktrace($frame = null) + { + if (defined('PEAR_IGNORE_BACKTRACE')) { + return null; + } + if ($frame === null) { + return $this->backtrace; + } + return $this->backtrace[$frame]; + } + + // }}} + // {{{ addUserInfo() + + function addUserInfo($info) + { + if (empty($this->userinfo)) { + $this->userinfo = $info; + } else { + $this->userinfo .= " ** $info"; + } + } + + // }}} + // {{{ toString() + function __toString() + { + return $this->getMessage(); + } + // }}} + // {{{ toString() + + /** + * Make a string representation of this object. + * + * @return string a string with an object summary + * @access public + */ + function toString() { + $modes = array(); + $levels = array(E_USER_NOTICE => 'notice', + E_USER_WARNING => 'warning', + E_USER_ERROR => 'error'); + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_array($this->callback)) { + $callback = (is_object($this->callback[0]) ? + strtolower(get_class($this->callback[0])) : + $this->callback[0]) . '::' . + $this->callback[1]; + } else { + $callback = $this->callback; + } + return sprintf('[%s: message="%s" code=%d mode=callback '. + 'callback=%s prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + $callback, $this->error_message_prefix, + $this->userinfo); + } + if ($this->mode & PEAR_ERROR_PRINT) { + $modes[] = 'print'; + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + $modes[] = 'trigger'; + } + if ($this->mode & PEAR_ERROR_DIE) { + $modes[] = 'die'; + } + if ($this->mode & PEAR_ERROR_RETURN) { + $modes[] = 'return'; + } + return sprintf('[%s: message="%s" code=%d mode=%s level=%s '. + 'prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + implode("|", $modes), $levels[$this->level], + $this->error_message_prefix, + $this->userinfo); + } + + // }}} +} + +/* + * Local Variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ diff --git a/include/pear/PEAR/FixPHP5PEARWarnings.php b/include/pear/PEAR/FixPHP5PEARWarnings.php new file mode 100644 index 0000000000000000000000000000000000000000..da77106f8ecd3e245f8de483df9a45d7d4755abe --- /dev/null +++ b/include/pear/PEAR/FixPHP5PEARWarnings.php @@ -0,0 +1,7 @@ +<?php +if ($skipmsg) { + $a = &new $ec($code, $mode, $options, $userinfo); +} else { + $a = &new $ec($message, $code, $mode, $options, $userinfo); +} +?> diff --git a/include/pear/PEAR5.php b/include/pear/PEAR5.php new file mode 100644 index 0000000000000000000000000000000000000000..5cee090351ff0aac31f0ac628aeec8f69cef01f4 --- /dev/null +++ b/include/pear/PEAR5.php @@ -0,0 +1,33 @@ +<?php +/** + * This is only meant for PHP 5 to get rid of certain strict warning + * that doesn't get hidden since it's in the shutdown function + */ +class PEAR5 +{ + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR5::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + static function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + + return $properties[$class][$var]; + } +} diff --git a/include/staff/api.inc.php b/include/staff/api.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..7463ad0956cbfbb02d2e4d3faa82cfa0cd6da35e --- /dev/null +++ b/include/staff/api.inc.php @@ -0,0 +1,146 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + + +$info['phrase']=($errors && $_POST['phrase'])?Format::htmlchars($_POST['phrase']):$cfg->getAPIPassphrase(); +$select='SELECT * '; +$from='FROM '.API_KEY_TABLE; +$where=''; +$sortOptions=array('date'=>'created','ip'=>'ipaddr'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +//Sorting options... +if($_REQUEST['sort']) { + $order_column =$sortOptions[$_REQUEST['sort']]; +} + +if($_REQUEST['order']) { + $order=$orderWays[$_REQUEST['order']]; +} +$order_column=$order_column?$order_column:'ipaddr'; +$order=$order?$order:'ASC'; +$order_by=" ORDER BY $order_column $order "; + +$total=db_count('SELECT count(*) '.$from.' '.$where); +$pagelimit=1000;//No limit. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('admin.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +$query="$select $from $where $order_by"; +//echo $query; +$result = db_query($query); +$showing=db_num_rows($result)?$pageNav->showing():''; +$negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. +$deletable=0; +?> +<div class="msg">API Keys</div> +<hr> +<div><b><?php echo $showing; ?></b></div> + <table width="100%" border="0" cellspacing=1 cellpadding=2> + <form action="admin.php?t=api" method="POST" name="api" onSubmit="return checkbox_checker(document.forms['api'],1,0);"> + <input type=hidden name='t' value='api'> + <input type=hidden name='do' value='mass_process'> + <tr><td> + <table border="0" cellspacing=0 cellpadding=2 class="dtable" align="center" width="100%"> + <tr> + <th width="7px"> </th> + <th>API Key</th> + <th width="10" nowrap>Active</th> + <th width="100" nowrap> IP Address</th> + <th width="150" nowrap> + <a href="admin.php?t=api&sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Create Date <?php echo $negorder; ?>">Created</a></th> + </tr> + <?php + $class = 'row1'; + $total=0; + $active=$inactive=0; + $sids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($result && db_num_rows($result)): + $dtpl=$cfg->getDefaultTemplateId(); + while ($row = db_fetch_array($result)) { + $sel=false; + $disabled=''; + if($row['isactive']) + $active++; + else + $inactive++; + + if($sids && in_array($row['id'],$sids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr class="<?php echo $class; ?>" id="<?php echo $row['id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked':''; ?> + onClick="highLight(this.value,this.checked);"> + <td> <?php echo $row['apikey']; ?></td> + <td><?php echo $row['isactive']?'<b>Yes</b>':'No'; ?></td> + <td> <?php echo $row['ipaddr']; ?></td> + <td> <?php echo Format::db_datetime($row['created']); ?></td> + </tr> + <?php + $class = ($class =='row2') ?'row1':'row2'; + } //end of while. + else: //nothin' found!! ?> + <tr class="<?php echo $class; ?>"><td colspan=5><b>Query returned 0 results</b> <a href="admin.php?t=templates">Index list</a></td></tr> + <?php + endif; ?> + + </table> + </td></tr> + <?php + if(db_num_rows($result)>0): //Show options.. + ?> + <tr> + <td align="center"> + <?php + if($inactive) { ?> + <input class="button" type="submit" name="enable" value="Enable" + onClick='return confirm("Are you sure you want to ENABLE selected keys?");'> + <?php + } + if($active){ ?> + + <input class="button" type="submit" name="disable" value="Disable" + onClick='return confirm("Are you sure you want to DISABLE selected keys?");'> + <?php } ?> + + <input class="button" type="submit" name="delete" value="Delete" + onClick='return confirm("Are you sure you want to DELETE selected keys?");'> + </td> + </tr> + <?php + endif; + ?> + </form> + </table> + <br/> + <div class="msg">Add New IP</div> + <hr> + <div> + Add a new IP address. <font class="error"><?php echo $errors['ip']; ?></font> + <form action="admin.php?t=api" method="POST" > + <input type=hidden name='t' value='api'> + <input type=hidden name='do' value='add'> + New IP: + <input name="ip" size=30 value="<?php echo ($errors['ip'])?Format::htmlchars($_REQUEST['ip']):''; ?>" /> + <font class="error">* </font> + <input class="button" type="submit" name="add" value="Add"> + </form> + </div> + <br/> + <div class="msg">API Passphrase</div> + <hr> + <div> + Passphrase must be at least 3 words. Required to generate the api keys.<br/> + <form action="admin.php?t=api" method="POST" > + <input type=hidden name='t' value='api'> + <input type=hidden name='do' value='update_phrase'> + Phrase: + <input name="phrase" size=50 value="<?php echo Format::htmlchars($info['phrase']); ?>" /> + <font class="error">* <?php echo $errors['phrase']; ?></font> + <input class="button" type="submit" name="update" value="Submit"> + </form> + <br/><br/> + <div><i>Please note that changing the passprase does NOT invalidate existing keys. To regerate a key you need to delete and readd it.</i></div> + </div> diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..5a82ee39bf264a821de957a779c0a73087f78bd9 --- /dev/null +++ b/include/staff/apikey.inc.php @@ -0,0 +1,104 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($api && $_REQUEST['a']!='add'){ + $title='Update API Key'; + $action='update'; + $submit_text='Save Changes'; + $info['id']=$api->getId(); + $info['isactive']=$api->isActive()?1:0; + $info['notes']=$api->getNotes(); + $qstr.='&id='.$api->getId(); +}else { + $title='Add New API Key'; + $action='add'; + $submit_text='Add Key'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; + $qstr.='&a='.urlencode($_REQUEST['a']); +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="apikeys.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>API Key</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>API Key is autogenerated and unique per IP address.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* </span> + </td> + </tr> + <?php if($api){ ?> + <tr> + <td width="180"> + IP Address: + </td> + <td> + <?php echo $api->getIPAddr(); ?> + </td> + </tr> + <tr> + <td width="180"> + API Key: + </td> + <td><?php echo $api->getKey(); ?> <em>(Delete and re-add to change the key)</em></td> + </tr> + <?php }else{ ?> + <tr> + <td width="180" class="required"> + IP Address: + </td> + <td> + <input type="text" size="30" name="ipaddr" value="<?php echo $info['ipaddr']; ?>"> + <span class="error">* <?php echo $errors['ipaddr']; ?></span> + </td> + </tr> + <?php } ?> + <tr> + <th colspan="2"> + <em><strong>Enabled Services:</strong>: Check applicable API services.</em> + </th> + </tr> + <tr> + <td width="180"> + Email piping: + </td> + <td> + <input type="checkbox" name="email_piping" value="1" checked="checked" disabled="disabled" > + <strong>Enable</strong> remote email piping. + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="apikeys.php"'> +</p> +</form> diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..26468089d56016c34e8dfd1dfefc972f33854b29 --- /dev/null +++ b/include/staff/apikeys.inc.php @@ -0,0 +1,120 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT * FROM '.API_KEY_TABLE.' WHERE 1'; +$sortOptions=array('key'=>'apikey','status'=>'isactive','ip'=>'ipaddr','date'=>'created','created'=>'created','updated'=>'updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'date'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'key.created'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'DESC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.API_KEY_TABLE.' '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('apikeys.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' API Keys'; +else + $showing='No API keys found!'; + +?> + +<div style="width:700;padding-top:5px; float:left;"> + <h2>API Keys</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="apikeys.php?a=add" class="Icon newapi">Add New API Key</a></b></div> +<div class="clear"></div> +<form action="apikeys.php" method="POST" name="keys" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="150" nowrap><a <?php echo $date_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=date">Date Added</a></th> + <th width="320"><a <?php echo $key_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=key">API Key</a></th> + <th width="100"><a <?php echo $status_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="120"><a <?php echo $ip_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=ip">IP Addr.</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td> <?php echo Format::db_date($row['created']); ?></td> + <td> <a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td> + <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td><?php echo $row['ipaddr']; ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['keys'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['keys'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['keys'],true)">Toggle</a> + <?php }else{ + echo 'No API keys found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected API keys?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected API keys?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected API keys?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/attachment.inc.php b/include/staff/attachment.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..17819867e69a458517e7c312075c07ffe3eeb06c --- /dev/null +++ b/include/staff/attachment.inc.php @@ -0,0 +1,82 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); +//Get the config info. +$config=($errors && $_POST)?Format::input($_POST):$cfg->getConfig(); +?> +<table width="100%" border="0" cellspacing=0 cellpadding=0> + <form action="admin.php?t=attach" method="post"> + <input type="hidden" name="t" value="attach"> + <tr> + <td> + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"> + <td colspan=2> Attachments Settings</td> + </tr> + <tr class="subheader"> + <td colspan=2"> + Before enabling attachments make sure you understand the security settings and issues related to file uploads.</td> + </tr> + <tr> + <th width="165">Allow Attachments:</th> + <td> + <input type="checkbox" name="allow_attachments" <?php echo $config['allow_attachments'] ?'checked':''; ?>><b>Allow Attachments</b> + (<i>Global Setting</i>) + <font class="error"> <?php echo $errors['allow_attachments']; ?></font> + </td> + </tr> + <tr> + <th>Emailed Attachments:</th> + <td> + <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments'] ? 'checked':''; ?> > Accept emailed files + <font class="warn"> <?php echo $warn['allow_email_attachments']; ?></font> + </td> + </tr> + <tr> + <th>Online Attachments:</th> + <td> + <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments'] ?'checked':''; ?> > + Allow online attachments upload<br/> + <input type="checkbox" name="allow_online_attachments_onlogin" <?php echo $config['allow_online_attachments_onlogin'] ?'checked':''; ?> > + Authenticated users Only. (<i>User must be logged in to upload files </i>) + <font class="warn"> <?php echo $warn['allow_online_attachments']; ?></font> + </td> + </tr> + <tr> + <th>Staff Response Files:</th> + <td> + <input type="checkbox" name="email_attachments" <?php echo $config['email_attachments']?'checked':''; ?> >Email attachments to the user + </td> + </tr> + <tr> + <th nowrap>Maximum File Size:</th> + <td> + <input type="text" name="max_file_size" value="<?php echo $config['max_file_size']; ?>"> <i>bytes</i> + <font class="error"> <?php echo $errors['max_file_size']; ?></font> + </td> + </tr> + <tr> + <th>Attachment Folder:</th> + <td> + Web user (e.g apache) must have write access to the folder. <font class="error"> <?php echo $errors['upload_dir']; ?></font><br> + <input type="text" size=60 name="upload_dir" value="<?php echo $config['upload_dir']; ?>"> + <font color=red> + <?php echo $attwarn; ?> + </font> + </td> + </tr> + <tr> + <th valign="top"><br/>Accepted File Types:</th> + <td> + Enter file extensions allowed separated by a comma. e.g <i>.doc, .pdf, </i> <br> + To accept all files enter wildcard <b><i>.*</i></b> i.e dotStar (NOT recommended). + <textarea name="allowed_filetypes" cols="21" rows="4" style="width: 65%;" wrap=HARD ><?php echo $config['allowed_filetypes']; ?></textarea> + </td> + </tr> + </table> + </td></tr> + <tr><td style="padding:10px 0 10px 200px"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> + </td></tr> + </form> +</table> diff --git a/include/staff/banlist.inc.php b/include/staff/banlist.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..5978964273ed573ca42fa817ed7eaf348ae143cb --- /dev/null +++ b/include/staff/banlist.inc.php @@ -0,0 +1,145 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin() || !$filter) die('Access Denied'); + +$qstr=''; +$select='SELECT rule.* '; +$from='FROM '.EMAIL_FILTER_RULE_TABLE.' rule '; +$where='WHERE rule.filter_id='.db_input($filter->getId()); +$search=false; +if($_REQUEST['q'] && strlen($_REQUEST['q'])>3) { + $search=true; + if(strpos($_REQUEST['q'],'@') && Validator::is_email($_REQUEST['q'])) + $where.=' AND rule.val='.db_input($_REQUEST['q']); + else + $where.=' AND rule.val LIKE "%'.db_input($_REQUEST['q'],false).'%"'; + +}elseif($_REQUEST['q']) { + $errors['q']='Term too short!'; +} + +//TODO: Add search here.. + +$sortOptions=array('email'=>'rule.val','status'=>'isactive','created'=>'rule.created','created'=>'rule.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'email'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'rule.val'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} + +$order=$order?$order:'ASC'; +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(DISTINCT rule.id) '.$from.' '.$where); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('banlist.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$select $from $where ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +//echo $query; +?> +<h2>Banned Email Addresses</h2> +<div style="width:600; float:left;padding-top:5px;"> + <form action="banlist.php" method="GET" name="filter"> + <input type="hidden" name="a" value="filter" > + <div> + Query: <input name="q" type="text" size="20" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>"> + + <input type="submit" name="submit" value="Search"/> + </div> + </form> + </div> +<div style="float:right;text-align:right;padding-right:5px;"><b><a href="banlist.php?a=add" class="Icon newstaff">Ban New Email</a></b></div> +<div class="clear"></div> +<?php +if(($res=db_query($query)) && ($num=db_num_rows($res))) + $showing=$pageNav->showing(); +else + $showing='No banned emails matching the query found!'; + +if($search) + $showing='Search Results: '.$showing; + +?> +<form action="banlist.php" method="POST" name="banlist" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7px"> </th> + <th width="350"><a <?php echo $email_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=email">Email Address</a></th> + <th width="200"><a <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status">Ban Status</a></th> + <th width="120"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created">Date Added</a></th> + <th width="120"><a <?php echo $updated_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + if($res && db_num_rows($res)): + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?> + onClick="highLight(this.value,this.checked);"> + <td> <a href="banlist.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['val']); ?></a></td> + <td> <?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td><?php echo Format::db_date($row['created']); ?></td> + <td><?php echo Format::db_datetime($row['updated']); ?> </td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="5"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['banlist'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['banlist'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['banlist'],true)">Toggle</a> + <?php }else{ + echo 'No banned emails found!'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected email ban?");'> + + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected emails ban?");'> + + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected emails?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/banrule.inc.php b/include/staff/banrule.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..bd28d19dea8368b395122beeb861045205d7ddff --- /dev/null +++ b/include/staff/banrule.inc.php @@ -0,0 +1,79 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$info=array(); +$qstr=''; +if($rule && $_REQUEST['a']!='add'){ + $title='Update Ban Rule'; + $action='update'; + $submit_text='Update'; + $info=$rule->getInfo(); + $info['id']=$rule->getId(); + $qstr.='&id='.$rule->getId(); +}else { + $title='Add New Email Address to Ban List'; + $action='add'; + $submit_text='Add'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; + $qstr.='&a='.urlencode($_REQUEST['a']); +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="banlist.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Manage Email Ban List</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Valid email address required.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Filter Name: + </td> + <td><?php echo $filter->getName(); ?></td> + </tr> + <tr> + <td width="180" class="required"> + Ban Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Email Address: + </td> + <td> + <input name="val" type="text" size="24" value="<?php echo $info['val']; ?>"> + <span class="error">* <?php echo $errors['val']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Internal notes</strong>: Admin notes </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="banlist.php"'> +</p> +</form> diff --git a/include/staff/cannedreplies.inc.php b/include/staff/cannedreplies.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..db44a79f3cc9470cdbdbd0746bd589a5bc16b44f --- /dev/null +++ b/include/staff/cannedreplies.inc.php @@ -0,0 +1,129 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied'); + +$qstr=''; +$sql='SELECT canned.*, count(attach.file_id) as files, dept.dept_name as department '. + ' FROM '.CANNED_TABLE.' canned '. + ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=canned.dept_id) '. + ' LEFT JOIN '.CANNED_ATTACHMENT_TABLE.' attach ON (attach.canned_id=canned.canned_id) '; +$sql.=' WHERE 1'; + +$sortOptions=array('title'=>'canned.title','status'=>'canned.isenabled','dept'=>'department','updated'=>'canned.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'title'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} + +$order_column=$order_column?$order_column:'canned.title'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} + +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} + +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.CANNED_TABLE.' canned '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('canned.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY canned.canned_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' premade responses'; +else + $showing='No premade responses found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>Canned Replies</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="canned.php?a=add" class="Icon newHelpTopic">Add New Reply</a></b></div> +<div class="clear"></div> +<form action="canned.php" method="POST" name="canned" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="500"><a <?php echo $title_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=title">Title</a></th> + <th width="80"><a <?php echo $status_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="200"><a <?php echo $dept_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=dept">Department</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="canned.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultId=$cfg->getDefaultDeptId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['canned_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + $files=$row['files']?'<span class="Icon file"> </span>':''; + ?> + <tr id="<?php echo $row['canned_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['canned_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> + onClick="highLight(this.value,this.checked);"> </td> + <td> + <a href="canned.php?id=<?php echo $row['canned_id']; ?>"><?php echo Format::truncate($row['title'],200); echo " $files"; ?></a> + </td> + <td><?php echo $row['isenabled']?'Active':'<b>Disabled</b>'; ?></td> + <td><?php echo $row['department']?$row['department']:'-- All Departments --'; ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="5"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['canned'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['canned'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['canned'],true)">Toggle</a> + <?php }else{ + echo 'No premade replies'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected replies?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected replies?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected replies?");'> +</p> +<?php +endif; +?> +</form> diff --git a/include/staff/cannedreply.inc.php b/include/staff/cannedreply.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..fa8e4615a9f91e4cdfc705e0ca261b9ab67886f6 --- /dev/null +++ b/include/staff/cannedreply.inc.php @@ -0,0 +1,115 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied'); +$info=array(); +$qstr=''; +if($canned && $_REQUEST['a']!='add'){ + $title='Update Canned Reply'; + $action='update'; + $submit_text='Save Changes'; + $info=$canned->getInfo(); + $info['id']=$canned->getId(); + $qstr.='&id='.$canned->getId(); +}else { + $title='Add New Canned Reply'; + $action='create'; + $submit_text='Add Reply'; + $info['isenabled']=isset($info['isenabled'])?$info['isenabled']:1; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); + +?> +<form action="canned.php?<?php echo $qstr; ?>" method="post" id="save" enctype="multipart/form-data"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Canned Reply</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Canned reply settings</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Status:</td> + <td> + <input type="radio" name="isenabled" value="1" <?php echo $info['isenabled']?'checked="checked"':''; ?>>Active + <input type="radio" name="isenabled" value="0" <?php echo !$info['isenabled']?'checked="checked"':''; ?>>Disabled + <span class="error">* <?php echo $errors['isenabled']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required">Department:</td> + <td> + <select name="dept_id"> + <option value="0">— All Departments —</option> + <?php + $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)) { + while(list($id,$name)=db_fetch_row($res)) { + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['dept_id']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Canned Reply</strong>: Make the title short and clear. </em> + </th> + </tr> + <tr> + <td colspan=2> + <div><b>Title</b><span class="error">* <?php echo $errors['title']; ?></span></div> + <input type="text" size="70" name="title" value="<?php echo $info['title']; ?>"> + <br><br><div><b>Canned Response</b> <font class="error">* <?php echo $errors['response']; ?></font> + (<a class="tip" href="ticket_variables">Supported Variables</a>)</div> + <textarea name="response" cols="21" rows="12" style="width: 80%;"><?php echo $info['response']; ?></textarea> + <br><br><div><b>Canned Attachments</b> (optional) <font class="error"> <?php echo $errors['files']; ?></font></div> + <?php + if($canned && ($files=$canned->getAttachments())) { + echo '<div id="canned_attachments"><span class="faded">Uncheck to delete the attachment on submit</span><br>'; + foreach($files as $file) { + $hash=$file['hash'].md5($file['id'].session_id()); + echo sprintf('<label><input type="checkbox" name="files[]" id="f%d" value="%d" checked="checked"> + <a href="file.php?h=%s">%s</a> </label> ', + $file['id'], $file['id'], $hash, $file['name']); + } + echo '</div><br>'; + + } + //Hardcoded limit... TODO: add a setting on admin panel - what happens on tickets page?? + if(count($files)<10) { + ?> + <div> + <input type="file" name="attachments[]" value=""/> + </div> + <?}?> + <div class="faded">You can upload up to 10 attachments per canned response.</div> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Internal Notes</strong>: Notes about the canned reply. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="canned.php"'> +</p> +</form> diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..bba679898b0c5b8891486faa657eb73114f50f1f --- /dev/null +++ b/include/staff/categories.inc.php @@ -0,0 +1,124 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied'); + +$qstr=''; +$sql='SELECT cat.category_id, cat.name, cat.ispublic, cat.updated, count(faq.faq_id) as faqs '. + ' FROM '.FAQ_CATEGORY_TABLE.' cat '. + ' LEFT JOIN '.FAQ_TABLE.' faq ON (faq.category_id=cat.category_id) '; +$sql.=' WHERE 1'; +$sortOptions=array('name'=>'cat.name','type'=>'cat.ispublic','faqs'=>'faqs','updated'=>'cat.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'cat.name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.FAQ_CATEGORY_TABLE.' cat '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('categories.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY cat.category_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' categories'; +else + $showing='No FAQ categories found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>FAQ Categories</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="categories.php?a=add" class="Icon newHelpTopic">Add New Category</a></b></div> +<div class="clear"></div> +<form action="categories.php" method="POST" name="cat" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="500"><a <?php echo $name_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="150"><a <?php echo $type_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=type">Type</a></th> + <th width="80"><a <?php echo $faqs_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=faqs">FAQs</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="categories.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultId=$cfg->getDefaultDeptId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['category_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + $faqs=0; + if($row['faqs']) + $faqs=sprintf('<a href="faq.php?cid=%d">%d</a>',$row['category_id'],$row['faqs']); + + ?> + <tr id="<?php echo $row['category_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['category_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> + onClick="highLight(this.value,this.checked);"> </td> + <td><a href="categories.php?id=<?php echo $row['category_id']; ?>"><?php echo Format::truncate($row['name'],200); ?></a> </td> + <td><?php echo $row['ispublic']?'<b>Public</b>':'Internal'; ?></td> + <td style="text-align:right;padding-right:25px;"><?php echo $faqs; ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="5"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['cat'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['cat'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['cat'],true)">Toggle</a> + <?php }else{ + echo 'No FAQ categories found.'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="public" value="Make Public" + onClick=' return confirm("Are you sure you want to make selected categories PUBLIC?");'> + <input class="button" type="submit" name="private" value="Make Internal" + onClick=' return confirm("Are you sure you want to make selected replies INTERNAL?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected categories - including associated FAQs?");'> +</p> +<?php +endif; +?> +</form> diff --git a/include/staff/category.inc.php b/include/staff/category.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..8272d7da13e24f1531b577fd051dccbfc487b108 --- /dev/null +++ b/include/staff/category.inc.php @@ -0,0 +1,79 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canManageFAQ()) die('Access Denied'); +$info=array(); +$qstr=''; +if($category && $_REQUEST['a']!='add'){ + $title='Update Category :'.$category->getName(); + $action='update'; + $submit_text='Save Changes'; + $info=$category->getHashtable(); + $info['id']=$category->getId(); + $qstr.='&id='.$category->getId(); +}else { + $title='Add New Category'; + $action='create'; + $submit_text='Add'; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); + +?> +<form action="categories.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>FAQ Category</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + </th> + </tr> + </thead> + <tbody> + <tr> + <th colspan="2"> + <em>Category information: Public categories are published if it has published FAQ articles.</em> + </th> + </tr> + <tr> + <td width="180" class="required">Category Type:</td> + <td> + <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><b>Public</b> (publish) + + <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>>Private (internal) + <span class="error">* <?php echo $errors['ispublic']; ?></span> + </td> + </tr> + <tr> + <td colspan=2> + <div style="padding-top:3px;"><b>Category Name</b>: <span class="faded">Short descriptive name.</span></div> + <input type="text" size="70" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + <br> + <div style="padding-top:5px;"> + <b>Category Description</b>: <span class="faded">Summary of the category.</span> + + <font class="error">* <?php echo $errors['description']; ?></font></div> + <textarea class="richtext" name="description" cols="21" rows="12" style="width:98%;"><?php echo $info['description']; ?></textarea> + </td> + </tr> + <tr> + <th colspan="2"> + <em>Internal Notes </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="categories.php"'> +</p> +</form> diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..fa32ec2795d1ab1635fa126ab639a5d1259f3258 --- /dev/null +++ b/include/staff/department.inc.php @@ -0,0 +1,210 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($dept && $_REQUEST['a']!='add'){ + //Editing Department. + $title='Update Department'; + $action='update'; + $submit_text='Save Changes'; + $info=$dept->getInfo(); + $info['id']=$dept->getId(); + $qstr.='&id='.$dept->getId(); +}else { + $title='Add New Department'; + $action='create'; + $submit_text='Create Dept'; + $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1; + $info['ticket_auto_response']=isset($info['ticket_auto_response'])?$info['ticket_auto_response']:1; + $info['message_auto_response']=isset($info['message_auto_response'])?$info['message_auto_response']:1; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="departments.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Department</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Department Information</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Type: + </td> + <td> + <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><strong>Public</strong> + <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><strong>Private</strong> (Internal) + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Email: + </td> + <td> + <select name="email_id"> + <option value="0">— Select Department Email —</option> + <?php + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$email,$name)=db_fetch_row($res)){ + $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':''; + if($name) + $email=Format::htmlchars("$name <$email>"); + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['email_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Template: + </td> + <td> + <select name="tpl_id"> + <option value="0">— System default —</option> + <?php + $sql='SELECT tpl_id,name FROM '.EMAIL_TEMPLATE_TABLE.' tpl WHERE isactive=1 ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['tpl_id'] && $id==$info['tpl_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['tpl_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + SLA: + </td> + <td> + <select name="sla_id"> + <option value="0">— System default —</option> + <?php + $sql='SELECT id,name FROM '.SLA_TABLE.' sla ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['sla_id'] && $id==$info['sla_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['sla_id']; ?></span> + </td> + </tr> + <?php + if($dept && $dept->getNumUsers()){ ?> + <tr> + <td width="180" class="required"> + Manager: + </td> + <td> + <select name="manager_id"> + <option value="0">— None —</option> + <option value="0" disabled="disabled">Select Department Manager (Optional)</option> + <?php + $sql='SELECT staff_id,CONCAT_WS(" ",firstname,lastname) as name FROM '.STAFF_TABLE.' staff '. + 'WHERE dept_id='.db_input($dept->getId()).' ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['manager_id'] && $id==$info['manager_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error"> <?php echo $errors['manager_id']; ?></span> + </td> + </tr> + <?php } ?> + <tr> + <th colspan="2"> + <em><strong>Auto Response Settings</strong>: Overwrite global auto-response settings for tickets routed to the Dept.</em> + </th> + </tr> + <tr> + <td width="180"> + New Ticket: + </td> + <td> + <input type="checkbox" name="ticket_auto_response" value="0" <?php echo !$info['ticket_auto_response']?'checked="checked"':''; ?> > + + <strong>Disable</strong> new ticket auto-response for this Dept. + </td> + </tr> + <tr> + <td width="180"> + New Message: + </td> + <td> + <input type="checkbox" name="message_auto_response" value="0" <?php echo !$info['message_auto_response']?'checked="checked"':''; ?> > + <strong>Disable</strong> new message auto-response for this Dept. + </td> + </tr> + <tr> + <td width="180"> + Auto Response Email: + </td> + <td> + <select name="autoresp_email_id"> + <option value="" disabled="disabled">Select Outgoing Email</option> + <option value="0">— Department Email (Above) —</option> + <?php + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$email,$name)=db_fetch_row($res)){ + $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':''; + if($name) + $email=Format::htmlchars("$name <$email>"); + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email); + } + } + ?> + </select> + <span class="error"> <?php echo $errors['autoresp_email_id']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Department Signature</strong>: Optional signature used on outgoing emails. <span class="error"> <?php echo $errors['signature']; ?></span></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="signature" cols="21" rows="5" style="width: 60%;"><?php echo $info['signature']; ?></textarea> + <br><em>Signature is made available as a choice, on ticket reply, for public departments.</em> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="departments.php"'> +</p> +</form> diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..805b7ec20706c7cbbfa1bb2dd331068aa3bc8154 --- /dev/null +++ b/include/staff/departments.inc.php @@ -0,0 +1,130 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT dept.dept_id,dept_name,email.email_id,email.email,email.name as email_name,ispublic,count(staff.staff_id) as users '. + ',CONCAT_WS(" ",mgr.firstname,mgr.lastname) as manager,mgr.staff_id as manager_id,dept.created,dept.updated FROM '.DEPT_TABLE.' dept '. + ' LEFT JOIN '.STAFF_TABLE.' mgr ON dept.manager_id=mgr.staff_id '. + ' LEFT JOIN '.EMAIL_TABLE.' email ON dept.email_id=email.email_id '. + ' LEFT JOIN '.STAFF_TABLE.' staff ON dept.dept_id=staff.dept_id '; + +$sql.=' WHERE 1'; +$sortOptions=array('name'=>'dept.dept_name','type'=>'ispublic','users'=>'users','email'=>'email_name, email.email','manager'=>'manager'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'dept.dept_name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); + +$query="$sql GROUP BY dept.dept_id ORDER BY $order_by"; +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing="Showing 1-$num of $num departments"; +else + $showing='No departments found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>Departments</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="departments.php?a=add" class="Icon newDepartment">Add New Department</a></b></div> +<div class="clear"></div> +<form action="departments.php" method="POST" name="depts" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7px"> </th> + <th width="180"><a <?php echo $name_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="80"><a <?php echo $type_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=type">Type</a></th> + <th width="70"><a <?php echo $users_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=users">Users</a></th> + <th width="300"><a <?php echo $email_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=email">Email Address</a></th> + <th width="200"><a <?php echo $manager_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=manager">Dept. Manager</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultId=$cfg->getDefaultDeptId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['dept_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + $row['email']=$row['email_name']?($row['email_name'].' <'.$row['email'].'>'):$row['email']; + $default=($defaultId==$row['dept_id'])?' <small>(Default)</small>':''; + ?> + <tr id="<?php echo $row['dept_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['dept_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> + onClick="highLight(this.value,this.checked);"> </td> + <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['dept_name']; ?></a> <?php echo $default; ?></td> + <td><?php echo $row['ispublic']?'Public':'<b>Private</b>'; ?></td> + <td> + <b> + <?php if($row['users']>0) { ?> + <a href="staff.php?did=<?php echo $row['dept_id']; ?>"><?php echo $row['users']; ?></a> + <?php }else{ ?> 0 + <?php } ?> + </b> + </td> + <td><a href="emails.php?id=<?php echo $row['email_id']; ?>"><?php echo $row['email']; ?></a></td> + <td><a href="staff.php?id=<?php echo $row['manager_id']; ?>"><?php echo $row['manager']; ?> </a></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['depts'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['depts'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['depts'],true)">Toggle</a> + <?php }else{ + echo 'No department found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. +?> +<p class="centered"> + <input class="button" type="submit" name="public" value="Make Public" + onClick=' return confirm("Are you sure you want to make selected departments public?");'> + <input class="button" type="submit" name="private" value="Make Private" + onClick=' return confirm("Are you sure you want to make selected departments private?");'> + <input class="button" type="submit" name="delete" value="Delete Dept(s)" + onClick=' return confirm("Are you sure you want to DELETE selected departments?");'> +</p> +<?php +endif; +?> + +</form> + diff --git a/include/staff/directory.inc.php b/include/staff/directory.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..b687d811f2a8993c7eb8753fadb4efe5fe7668ad --- /dev/null +++ b/include/staff/directory.inc.php @@ -0,0 +1,137 @@ +<?php +if(!defined('OSTSTAFFINC') || !$thisstaff || !$thisstaff->isStaff()) die('Access Denied'); +$qstr=''; +$select='SELECT staff.*,CONCAT_WS(" ",firstname,lastname) as name,dept.dept_name as dept '; +$from='FROM '.STAFF_TABLE.' staff '. + 'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.dept_id) '; +$where='WHERE staff.isvisible=1 '; + +if($_REQUEST['q']) { + $searchTerm=$_REQUEST['q']; + if($searchTerm){ + $query=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes. + if(is_numeric($searchTerm)){ + $where.=" AND (staff.phone LIKE '%$query%' OR staff.phone_ext LIKE '%$query%' staff.mobile LIKE '%$query%') "; + }elseif(strpos($searchTerm,'@') && Validator::is_email($searchTerm)){ + $where.=" AND staff.email='$query'"; + }else{ + $where.=" AND ( staff.email LIKE '%$query%'". + " OR staff.lastname LIKE '%$query%'". + " OR staff.firstname LIKE '%$query%'". + ' ) '; + } + } +} + +if($_REQUEST['did'] && is_numeric($_REQUEST['did'])) { + $where.=' AND staff.dept_id='.db_input($_REQUEST['did']); + $qstr.='&did='.urlencode($_REQUEST['did']); +} + +$sortOptions=array('name'=>'staff.firstname,staff.lastname','email'=>'staff.email','dept'=>'dept.dept_name', + 'phone'=>'staff.phone','mobile'=>'staff.mobile','ext'=>'phone_ext', + 'created'=>'staff.created','login'=>'staff.lastlogin'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'staff.firstname,staff.lastname'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} + +$order=$order?$order:'ASC'; +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(DISTINCT staff.staff_id) '.$from.' '.$where); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('directory.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$select $from $where GROUP BY staff.staff_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +//echo $query; +?> +<h2>Staff Members</h2> +<div style="width:700; float:left;"> + <form action="directory.php" method="GET" name="filter"> + <input type="text" name="q" value="<?php echo $_REQUEST['q']; ?>" > + <select name="did" id="did"> + <option value="0">— All Department —</option> + <?php + $sql='SELECT dept.dept_id, dept.dept_name,count(staff.staff_id) as users '. + 'FROM '.DEPT_TABLE.' dept '. + 'INNER JOIN '.STAFF_TABLE.' staff ON(staff.dept_id=dept.dept_id AND staff.isvisible=1) '. + 'GROUP By dept.dept_id HAVING users>0 ORDER BY dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name, $users)=db_fetch_row($res)){ + $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users); + } + } + ?> + </select> + + <input type="submit" name="submit" value="Filter"/> + </form> + </div> +<div class="clear"></div> +<?php +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing(); +else + $showing='No staff members found!'; +?> +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="160"><a <?php echo $name_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="150"><a <?php echo $dept_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=dept">Department</a></th> + <th width="180"><a <?php echo $email_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=email">Email Address</a></th> + <th width="120"><a <?php echo $phone_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=phone">Phone Number</a></th> + <th width="80"><a <?php echo $ext_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=ext">Phone Ext</a></th> + <th width="120"><a <?php echo $mobile_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=mobile">Mobile Number</a></th> + </tr> + </thead> + <tbody> + <?php + if($res && db_num_rows($res)): + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + while ($row = db_fetch_array($res)) { ?> + <tr id="<?php echo $row['staff_id']; ?>"> + <td> <?php echo Format::htmlchars($row['name']); ?></td> + <td> <?php echo Format::htmlchars($row['dept']); ?></td> + <td> <?php echo Format::htmlchars($row['email']); ?></td> + <td> <?php echo Format::phone($row['phone']); ?></td> + <td> <?php echo $row['phone_ext']; ?></td> + <td> <?php echo Format::phone($row['mobile']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num) { + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; + ?> + <?php } else { + echo 'No staff members found!'; + } ?> + </td> + </tr> + </tfoot> +</table> + diff --git a/include/staff/editticket.inc.php b/include/staff/editticket.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..20accf0e4d70883861b51e2c6518fa6bce060cdc --- /dev/null +++ b/include/staff/editticket.inc.php @@ -0,0 +1,142 @@ +<?php +if(!defined('OSTSCPINC') || !is_object($ticket) || !is_object($thisstaff) || !$thisstaff->isStaff()) die('Access Denied'); + +if(!($thisstaff->canEditTickets() || ($thisstaff->isManager() && $ticket->getDeptId()==$thisstaff->getDeptId()))) die('Access Denied. Perm error.'); + +if($_POST && $errors){ + $info=Format::input($_POST); +}else{ + $info=array('email'=>$ticket->getEmail(), + 'name' =>$ticket->getName(), + 'phone'=>$ticket->getPhone(), + 'phone_ext'=>$ticket->getPhoneExt(), + 'pri'=>$ticket->getPriorityId(), + 'topicId'=>$ticket->getTopicId(), + 'topic'=>$ticket->getHelpTopic(), + 'subject' =>$ticket->getSubject(), + 'duedate' =>$ticket->getDueDate()?(Format::userdate('m/d/Y',Misc::db2gmtime($ticket->getDueDate()))):'', + 'time'=>$ticket->getDueDate()?(Format::userdate('G:i',Misc::db2gmtime($ticket->getDueDate()))):'', + ); + /*Note: Please don't make me explain how dates work - it is torture. Trust me! */ +} + +?> +<div width="100%"> + <?php if($errors['err']) { ?> + <p align="center" id="errormessage"><?php echo $errors['err']; ?></p> + <?php }elseif($msg) { ?> + <p align="center" class="infomessage"><?php echo $msg; ?></p> + <?php }elseif($warn) { ?> + <p class="warnmessage"><?php echo $warn; ?></p> + <?php } ?> +</div> +<table width="100%" border="0" cellspacing=1 cellpadding=2> + <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post"> + <input type='hidden' name='id' value='<?php echo $ticket->getId(); ?>'> + <input type='hidden' name='a' value='update'> + <tr><td align="left" colspan=2 class="msg"> + Update Ticket #<?php echo $ticket->getExtId(); ?> (<a href="tickets.php?id=<?php echo $ticket->getId(); ?>" style="color:black;">View Ticket</a>)<br></td></tr> + <tr> + <td align="left" nowrap width="120"><b>Email Address:</b></td> + <td> + <input type="text" id="email" name="email" size="25" value="<?php echo $info['email']; ?>"> + <font class="error"><b>*</b> <?php echo $errors['email']; ?></font> + </td> + </tr> + <tr> + <td align="left" ><b>Full Name:</b></td> + <td> + <input type="text" id="name" name="name" size="25" value="<?php echo $info['name']; ?>"> + <font class="error"><b>*</b> <?php echo $errors['name']; ?></font> + </td> + </tr> + <tr> + <td align="left"><b>Subject:</b></td> + <td> + <input type="text" name="subject" size="35" value="<?php echo $info['subject']; ?>"> + <font class="error">* <?php echo $errors['subject']; ?></font> + </td> + </tr> + <tr> + <td align="left">Telephone:</td> + <td><input type="text" name="phone" size="25" value="<?php echo $info['phone']; ?>"> + Ext <input type="text" name="phone_ext" size="6" value="<?php echo $info['phone_ext']; ?>"> + <font class="error"> <?php echo $errors['phone']; ?></font></td> + </tr> + <tr height=1px><td align="left" colspan=2 > </td></tr> + <tr> + <td align="left" valign="top">Due Date:</td> + <td> + <i>Time is based on your time zone (GM <?php echo $thisstaff->getTZoffset(); ?>)</i> <font class="error"> <?php echo $errors['time']; ?></font><br> + <input id="duedate" name="duedate" value="<?php echo Format::htmlchars($info['duedate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF> + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('duedate')); return false;"><img src='images/cal.png'border=0 alt=""></a> + + <?php + $min=$hr=null; + if($info['time']) + list($hr,$min)=explode(':',$info['time']); + echo Misc::timeDropdown($hr,$min,'time'); + ?> + <font class="error"> <?php echo $errors['duedate']; ?></font> + </td> + </tr> + <?php + $sql='SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE.' ORDER BY priority_urgency DESC'; + if(($priorities=db_query($sql)) && db_num_rows($priorities)){ ?> + <tr> + <td align="left">Priority:</td> + <td> + <select name="pri"> + <?php + while($row=db_fetch_array($priorities)){ ?> + <option value="<?php echo $row['priority_id']; ?>" <?php echo $info['pri']==$row['priority_id']?'selected':''; ?> ><?php echo $row['priority_desc']; ?></option> + <?php } ?> + </select> + </td> + </tr> + <?php } ?> + + <?php + $services= db_query('SELECT topic_id,topic,isactive FROM '.TOPIC_TABLE.' ORDER BY topic'); + if($services && db_num_rows($services)){ ?> + <tr> + <td align="left" valign="top">Help Topic:</td> + <td> + <select name="topicId"> + <option value="0" selected >None</option> + <?php if(!$info['topicId'] && $info['topic']){ //old helptopic ?> + <option value="0" selected ><?php echo $info['topic']; ?> (deleted)</option> + <?php + } + while (list($topicId,$topic,$active) = db_fetch_row($services)){ + $selected = ($info['topicId']==$topicId)?'selected':''; + $status=$active?'Active':'Inactive'; + ?> + <option value="<?php echo $topicId; ?>"<?php echo $selected; ?>><?php echo $topic; ?> (<?php echo $status; ?>)</option> + <?php + } ?> + </select> + (optional)<font class="error"> <?php echo $errors['topicId']; ?></font> + </td> + </tr> + <?php + } ?> + <tr> + <td align="left" valign="top"><b>Internal Note:</b></td> + <td> + <i>Reasons for the edit.</i><font class="error"><b>* <?php echo $errors['note']; ?></b></font><br/> + <textarea name="note" cols="45" rows="5" wrap="soft"><?php echo $info['note']; ?></textarea></td> + </tr> + <tr height=2px><td align="left" colspan=2 > </td></tr> + <tr> + <td></td> + <td> + <input class="button" type="submit" name="submit_x" value="Update Ticket"> + <input class="button" type="reset" value="Reset"> + <input class="button" type="button" name="cancel" value="Cancel" onClick='window.location.href="tickets.php?id=<?php echo $ticket->getId(); ?>"'> + </td> + </tr> + </form> +</table> + diff --git a/include/staff/email.inc.php b/include/staff/email.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..4f2d695b09304c4dcd5fc850e32ede734c514bbf --- /dev/null +++ b/include/staff/email.inc.php @@ -0,0 +1,257 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($email && $_REQUEST['a']!='add'){ + $title='Update Email'; + $action='update'; + $submit_text='Save Changes'; + $info=$email->getInfo(); + $info['id']=$email->getId(); + if($info['mail_delete']) + $info['postfetch']='delete'; + elseif($info['mail_archivefolder']) + $info['postfetch']='archive'; + else + $info['postfetch']=''; //nothing. + if($info['userpass']) + $passwdtxt='To change password enter new password above.'; + + $qstr.='&id='.$email->getId(); +}else { + $title='Add New Email'; + $action='create'; + $submit_text='Submit'; + $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1; + $info['ticket_auto_response']=isset($info['ticket_auto_response'])?$info['ticket_auto_response']:1; + $info['message_auto_response']=isset($info['message_auto_response'])?$info['message_auto_response']:1; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<h2>Email Address</h2> +<form action="emails.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em><strong>Email Information</strong>: Login details are optional BUT required when IMAP/POP or SMTP are enabled.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Email Address + </td> + <td> + <input type="text" size="35" name="email" value="<?php echo $info['email']; ?>"> + <span class="error">* <?php echo $errors['email']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Email Name + </td> + <td> + <input type="text" size="35" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?> </span> + </td> + </tr> + <tr> + <td width="180"> + Login Username + </td> + <td> + <input type="text" size="35" name="userid" value="<?php echo $info['userid']; ?>"> + <span class="error"> <?php echo $errors['userid']; ?> </span> + </td> + </tr> + <tr> + <td width="180"> + Login Password + </td> + <td> + <input type="text" size="35" name="passwd" value="<?php echo $info['passwd']; ?>"> + <span class="error"> <?php echo $errors['passwd']; ?> </span> + <br><em><?php echo $passwdtxt; ?></em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Mail Account</strong>: Optional setting for fetching incoming emails. Mail fetching must be enabled with autocron active or external cron setup. <font class="error"> <?php echo $errors['mail']; ?></font></em> + </th> + </tr> + <tr><td>Status</td> + <td> + <label><input type="radio" name="mail_active" value="1" <?php echo $info['mail_active']?'checked="checked"':''; ?> /><strong>Enable</strong></label> + + <label><input type="radio" name="mail_active" value="0" <?php echo !$info['mail_active']?'checked="checked"':''; ?> />Disable</label> + <font class="error"> <?php echo $errors['mail_active']; ?></font> + </td> + </tr> + <tr><td>Host</td> + <td><input type="text" name="mail_host" size=35 value="<?php echo $info['mail_host']; ?>"> + <font class="error"> <?php echo $errors['mail_host']; ?></font> + </td> + </tr> + <tr><td>Port</td> + <td><input type="text" name="mail_port" size=6 value="<?php echo $info['mail_port']?$info['mail_port']:''; ?>"> + <font class="error"> <?php echo $errors['mail_port']; ?></font> + </td> + </tr> + <tr><td>Protocol</td> + <td> + <select name="mail_protocol"> + <option value='POP'>— Select Mail Protocol —</option> + <option value='POP' <?php echo ($info['mail_protocol']=='POP')?'selected="selected"':''; ?> >POP</option> + <option value='IMAP' <?php echo ($info['mail_protocol']=='IMAP')?'selected="selected"':''; ?> >IMAP</option> + </select> + <font class="error"> <?php echo $errors['mail_protocol']; ?></font> + </td> + </tr> + + <tr><td>Encryption</td> + <td> + <label><input type="radio" name="mail_encryption" value="NONE" + <?php echo ($info['mail_encryption']!='SSL')?'checked="checked"':''; ?> />None</label> + <label><input type="radio" name="mail_encryption" value="SSL" + <?php echo ($info['mail_encryption']=='SSL')?'checked="checked"':''; ?> />SSL</label> + <font class="error"> <?php echo $errors['mail_encryption']; ?></font> + </td> + </tr> + <tr><td>Fetch Frequency</td> + <td> + <input type="text" name="mail_fetchfreq" size=4 value="<?php echo $info['mail_fetchfreq']?$info['mail_fetchfreq']:''; ?>"> Delay intervals in minutes + <font class="error"> <?php echo $errors['mail_fetchfreq']; ?></font> + </td> + </tr> + <tr><td>Emails Per Fetch</td> + <td> + <input type="text" name="mail_fetchmax" size=4 value="<?php echo $info['mail_fetchmax']?$info['mail_fetchmax']:''; ?>"> Maximum emails to process per fetch. + <font class="error"> <?php echo $errors['mail_fetchmax']; ?></font> + </td> + </tr> + <tr> + <td width="180"> + New Ticket Priority: + </td> + <td> + <select name="priority_id"> + <option value="">— Select Priority —</option> + <?php + $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error"><?php echo $errors['priority_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + New Ticket Dept. + </td> + <td> + <select name="dept_id"> + <option value="">— Select Department —</option> + <?php + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error"><?php echo $errors['dept_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Auto-response + </td> + <td> + <input type="checkbox" name="noautoresp" value="1" <?php echo $info['noautoresp']?'checked="checked"':''; ?> > + <strong>Disable</strong> new ticket auto-response for this email. Overwrite global and dept. settings. + </td> + </tr> + <tr><td valign="top">Fetched Emails</td> + <td> + <input type="radio" name="postfetch" value="archive" <?php echo ($info['postfetch']=='archive')? 'checked="checked"': ''; ?> > + Move to: <input type="text" name="mail_archivefolder" size="20" value="<?php echo $info['mail_archivefolder']; ?>"/> folder. + <font class="error"> <?php echo $errors['mail_folder']; ?></font> + <input type="radio" name="postfetch" value="delete" <?php echo ($info['postfetch']=='delete')? 'checked="checked"': ''; ?> > + Delete fetched emails + <input type="radio" name="postfetch" value="" <?php echo (isset($info['postfetch']) && !$info['postfetch'])? 'checked="checked"': ''; ?> > + Do nothing (Not recommended) + <br><em>Moving fetched emails to a backup folder is highly recommended.</em> <font class="error"><?php echo $errors['postfetch']; ?></font> + </td> + </tr> + + <tr> + <th colspan="2"> + <em><strong>SMTP Settings</strong>: When enabled the <b>email account</b> will use SMTP server instead of internal PHP mail() function for outgoing emails. <font class="error"> <?php echo $errors['smtp']; ?></font></em> + </th> + </tr> + <tr><td>Status</td> + <td> + <label><input type="radio" name="smtp_active" value="1" <?php echo $info['smtp_active']?'checked':''; ?> />Enable</label> + <label><input type="radio" name="smtp_active" value="0" <?php echo !$info['smtp_active']?'checked':''; ?> />Disable</label> + <font class="error"> <?php echo $errors['smtp_active']; ?></font> + </td> + </tr> + <tr><td>SMTP Host</td> + <td><input type="text" name="smtp_host" size=35 value="<?php echo $info['smtp_host']; ?>"> + <font class="error"> <?php echo $errors['smtp_host']; ?></font> + </td> + </tr> + <tr><td>SMTP Port</td> + <td><input type="text" name="smtp_port" size=6 value="<?php echo $info['smtp_port']?$info['smtp_port']:''; ?>"> + <font class="error"> <?php echo $errors['smtp_port']; ?></font> + </td> + </tr> + <tr><td>Authentication Required?</td> + <td> + + <label><input type="radio" name="smtp_auth" value="1" + <?php echo $info['smtp_auth']?'checked':''; ?> />Yes</label> + <label><input type="radio" name="smtp_auth" value="0" + <?php echo !$info['smtp_auth']?'checked':''; ?> />NO</label> + <font class="error"> <?php echo $errors['smtp_auth']; ?></font> + </td> + </tr> + <tr> + <td>Allow Header Spoofing?</td> + <td> + <input type="checkbox" name="smtp_spoofing" value="1" <?php echo $info['smtp_spoofing'] ?'checked="checked"':''; ?>> + Allow email header spoofing <em>(only applies to emails being sent through this account)</em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Internal Notes</strong>: Admin's notes. <span class="error"> <?php echo $errors['notes']; ?></span></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="5" style="width: 60%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="emails.php"'> +</p> +</form> diff --git a/include/staff/emails.inc.php b/include/staff/emails.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..44e277b468ad9879edb7fe528b7ba960e0762858 --- /dev/null +++ b/include/staff/emails.inc.php @@ -0,0 +1,130 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT email.*,dept.dept_name as department,priority_desc as priority '. + ' FROM '.EMAIL_TABLE.' email '. + ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=email.dept_id) '. + ' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (pri.priority_id=email.priority_id) '; +$sql.=' WHERE 1'; +$sortOptions=array('email'=>'email.email','dept'=>'department','priority'=>'priority','created'=>'email.created','updated'=>'email.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'email'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'email.email'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.EMAIL_TABLE.' email '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('emails.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY email.email_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' emails'; +else + $showing='No emails found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>Email Addresses</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="emails.php?a=add" class="Icon newEmail">Add New Email</a></b></div> +<div class="clear"></div> +<form action="emails.php" method="POST" name="emails" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="400"><a <?php echo $email_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=email">Email</a></th> + <th width="120"><a <?php echo $priority_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=priority">Priority</a></th> + <th width="250"><a <?php echo $dept_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=dept">Department</a></th> + <th width="110" nowrap><a <?php echo $created_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=created">Created</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultId=$cfg->getDefaultEmailId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['email_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + $default=($row['email_id']==$defaultId); + $email=$row['email']; + if($row['name']) + $email=$row['name'].' <'.$row['email'].'>'; + ?> + <tr id="<?php echo $row['email_id']; ?>"> + <td width=7px> + <?php if($row['email_id']==$defaultId){ ?> + + <?php }else{ ?> + <input type="checkbox" name="ids[]" value="<?php echo $row['email_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> + onClick="highLight(this.value,this.checked);"> + <?php } ?> + </td> + <td><a href="emails.php?id=<?php echo $row['email_id']; ?>"><?php echo Format::htmlchars($email); ?></a> </td> + <td><?php echo $row['priority']; ?></td> + <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['department']; ?></a></td> + <td> <?php echo Format::db_date($row['created']); ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['emails'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['emails'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['emails'],true)">Toggle</a> + <?php }else{ + echo 'No help emails found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="delete" value="Delete Email(s)" + onClick=' return confirm("Are you sure you want to DELETE selected emails?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..cdbb266d106940fa267dd03743428832ed6656f6 --- /dev/null +++ b/include/staff/faq-view.inc.php @@ -0,0 +1,66 @@ +<?php +if(!defined('OSTSTAFFINC') || !$faq || !$thisstaff) die('Access Denied'); + +$category=$faq->getCategory(); + +?> +<h2>Frequently Asked Questions</h2> +<div id="breadcrumbs"> + <a href="kb.php">All Categories</a> + » <a href="kb.php?cid=<?php echo $category->getId(); ?>"><?php echo $category->getName(); ?></a> + <span class="faded">(<?php echo $category->isPublic()?'Public':'Internal'; ?>)</span> +</div> +<div style="width:700;padding-top:2px; float:left;"> +<strong style="font-size:16px;"><?php echo $faq->getQuestion() ?></strong> <span class="faded"><?php echo $faq->isPublished()?'(Published)':''; ?></span> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> +<?php +if($thisstaff->canManageFAQ()) { + echo sprintf('<a href="faq.php?id=%d&a=edit" class="Icon newHelpTopic">Edit FAQ</a>', + $faq->getId()); +} +?> + +</div> +<div class="clear"></div> +<p> +<?php echo Format::safe_html($faq->getAnswer()); ?> +</p> +<p> + <div><span class="faded"><b>Attachments:</b></span> <?php echo $faq->getAttachmentsLinks(); ?></div> + <div><span class="faded"><b>Help Topics:</b></span> + <?php echo ($topics=$faq->getHelpTopics())?implode(', ',$topics):' '; ?> + </div> +</p> +<div class="faded"> Last updated <?php echo Format::db_daydatetime($category->getUpdateDate()); ?></div> +<hr> +<?php +if($thisstaff->canManageFAQ()) { + //TODO: add js confirmation.... + ?> + <div> + <form action="faq.php?id=<?php echo $faq->getId(); ?>" method="post"> + <input type="hidden" name="id" value="<?php echo $faq->getId(); ?>"> + <input type="hidden" name="do" value="manage-faq"> + <div> + <strong>Options: </strong> + <select name="a" style="width:200px;"> + <option value="">Select Action</option> + <?php + if($faq->isPublished()) { ?> + <option value="unpublish">Unpublish FAQ</option> + <?php + }else{ ?> + <option value="publish">Publish FAQ</option> + <?php + } ?> + <option value="edit">Edit FAQ</option> + <option value="delete">Delete FAQ</option> + </select> + <input type="submit" name="submit" value="Go"> + </div> + </form> + </div> +<?php +} +?> diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..8882da2fa817cf1e5000d4700fab0cc2b9bb0a19 --- /dev/null +++ b/include/staff/faq.inc.php @@ -0,0 +1,150 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canManageFAQ()) die('Access Denied'); +$info=array(); +$qstr=''; +if($faq){ + $title='Update FAQ: '.$faq->getQuestion(); + $action='update'; + $submit_text='Save Changes'; + $info=$faq->getHashtable(); + $info['id']=$faq->getId(); + $info['topics']=$faq->getHelpTopicsIds(); + $qstr='id='.$faq->getId(); +}else { + $title='Add New FAQ'; + $action='create'; + $submit_text='Add FAQ'; + if($category) { + $qstr='cid='.$category->getId(); + $info['category_id']=$category->getId(); + } +} +//TODO: Add attachment support. +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="faq.php?<?php echo $qstr; ?>" method="post" id="save" enctype="multipart/form-data"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>FAQ</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + </th> + </tr> + </thead> + <tbody> + <tr> + <th colspan="2"> + <em>FAQ Information</em> + </th> + </tr> + <tr> + <td colspan=2> + <div style="padding-top:3px;"><b>Question</b> <span class="error">* <?php echo $errors['question']; ?></span></div> + <input type="text" size="70" name="question" value="<?php echo $info['question']; ?>"> + </td> + </tr> + <tr> + <td colspan=2> + <div><b>Category Listing</b>: <span class="faded">FAQ category the question belongs to.</span></div> + <select name="category_id" style="width:350px;"> + <option value="0">Select FAQ Category </option> + <?php + $sql='SELECT category_id, name, ispublic FROM '.FAQ_CATEGORY_TABLE; + if(($res=db_query($sql)) && db_num_rows($res)) { + while($row=db_fetch_array($res)) { + echo sprintf('<option value="%d" %s>%s (%s)</option>', + $row['category_id'], + (($info['category_id']==$row['category_id'])?'selected="selected"':''), + $row['name'], + ($info['ispublic']?'Public':'Internal')); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['category_id']; ?></span> + </td> + </tr> + <tr> + <td colspan=2> + <div><b>Listing Type</b>: + <span class="faded">Published questions are listed on public knowledgebase if the parent category is public.</span></div> + <input type="radio" name="ispublished" value="1" <?php echo $info['ispublished']?'checked="checked"':''; ?>>Public (publish) + + <input type="radio" name="ispublished" value="0" <?php echo !$info['ispublished']?'checked="checked"':''; ?>>Internal (private) + <span class="error">* <?php echo $errors['ispublished']; ?></span> + </td> + </tr> + <tr> + <td colspan=2> + <div> + <b>Answer</b> <font class="error">* <?php echo $errors['answer']; ?></font></div> + <textarea name="answer" cols="21" rows="12" style="width:98%;" class="richtext"><?php echo $info['answer']; ?></textarea> + </td> + </tr> + <tr> + <td colspan=2> + <div><b>Attachments</b> (optional) <font class="error"> <?php echo $errors['files']; ?></font></div> + <?php + if($faq && ($files=$faq->getAttachments())) { + echo '<div id="faq_attachments"><span class="faded">Uncheck to delete the attachment on submit</span><br>'; + foreach($files as $file) { + $hash=$file['hash'].md5($file['id'].session_id().$file['hash']); + echo sprintf('<label><input type="checkbox" name="files[]" id="f%d" value="%d" checked="checked"> + <a href="file.php?h=%s">%s</a> </label> ', + $file['id'], $file['id'], $hash, $file['name']); + } + echo '</div><br>'; + } + //TODO: add a setting on admin panel + if(count($files)<5) { + ?> + <div> + <input type="file" name="attachments[]" value=""/> + </div> + <?}?> + <div class="faded">You can upload up to 5 attachments.</div> + </td> + </tr> + <?php + $sql='SELECT topic_id, topic FROM '.TOPIC_TABLE; + if(($res=db_query($sql)) && db_num_rows($res)) { ?> + <tr> + <th colspan="2"> + <em><strong>Help Topics</strong>: Check all help topics related to this FAQ.</em> + </th> + </tr> + <tr><td> + <?php + while(list($topicId,$topic)=db_fetch_row($res)) { + echo sprintf('<input type="checkbox" name="topics[]" value="%d" %s>%s<br>', + $topicId, + (($info['topics'] && in_array($topicId,$info['topics']))?'checked="checked"':''), + $topic); + } + ?> + </td> + </tr> + <?php + } ?> + <tr> + <th colspan="2"> + <em><strong>Internal Notes</strong>: </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="faq.php?<?php echo $qstr; ?>"'> +</p> +</form> diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..91f74501661cda00d97aada035b28559a9961d41 --- /dev/null +++ b/include/staff/filter.inc.php @@ -0,0 +1,307 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$matches=array('name'=>"Sender's Name",'email'=>"Sender's Email",'subject'=>'Email Subject','body'=>'Email Body/Text','header'=>'Email Header'); +$match_types=array('equal'=>'Equal','not_equal'=>'Not Equal','contains'=>'Contains','dn_contain'=>'Does Not Contain'); + + +$info=array(); +$qstr=''; +if($filter && $_REQUEST['a']!='add'){ + $title='Update Filter'; + $action='update'; + $submit_text='Save Changes'; + $info=array_merge($filter->getInfo(),$filter->getFlatRules()); + $info['id']=$filter->getId(); + $qstr.='&id='.$filter->getId(); +}else { + $title='Add New Filter'; + $action='add'; + $submit_text='Add Filter'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:0; + $qstr.='&a='.urlencode($_REQUEST['a']); +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="filters.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Incoming Email Filter</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Filters are executed based on execution order.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Filter Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Execution Order: + </td> + <td> + <input type="text" size="6" name="execorder" value="<?php echo $info['execorder']; ?>"> + <em>(1...99 )</em> + <span class="error">* <?php echo $errors['execorder']; ?></span> + + <input type="checkbox" name="stop_onmatch" value="1" <?php echo $info['stop_onmatch']?'checked="checked"':''; ?> > + <strong>Stop</strong> processing further on match! + </td> + </tr> + <tr> + <td width="180" class="required"> + Filter Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180"> + To Email Address: + </td> + <td> + <select name="email_id"> + <option value="0">— Filter applies to ALL incoming emails ‐</option> + <?php + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$email,$name)=db_fetch_row($res)){ + $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':''; + if($name) + $email=Format::htmlchars("$name <$email>"); + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email); + } + } + ?> + </select> + <br><em>(Highly recommended if the filter is specific to one incoming email address)</em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Filter Rules</strong>: Rules are applied based on the criteria. + <span class="error">* <?php echo $errors['rules']; ?></span></em> + </th> + </tr> + <tr> + <td colspan=2> + <em>Rules Matching Criteria:</em> + + <input type="radio" name="match_all_rules" value="1" <?php echo $info['match_all_rules']?'checked="checked"':''; ?>>Match All + + <input type="radio" name="match_all_rules" value="0" <?php echo !$info['match_all_rules']?'checked="checked"':''; ?>>Match Any + <span class="error">* </span> + <em>(case-insensitive comparison)</em> + + </td> + </tr> + <?php + $n=($filter?$filter->getNumRules():0)+2; //2 extra rules of unlimited. + for($i=1; $i<=$n; $i++){ ?> + <tr id="r<?php echo $i; ?>"> + <td colspan="2"> + <div style="width:700; float:left;"> + <select name="rule_w<?php echo $i; ?>"> + <option value="">— Select One ‐</option> + <?php + foreach($matches as $k=>$v){ + $sel=($info["rule_w$i"]==$k)?'selected="selected"':''; + echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); + } + ?> + </select> + <select name="rule_h<?php echo $i; ?>"> + <option value="0">— Select One ‐</option> + <?php + foreach($match_types as $k=>$v){ + $sel=($info["rule_h$i"]==$k)?'selected="selected"':''; + echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); + } + ?> + </select> + <input type="text" size="30" name="rule_v<?php echo $i; ?>" value="<?php echo $info["rule_v$i"]; ?>"> + <span class="error"> <?php echo $errors["rule_$i"]; ?></span> + </div> + <?php + if($info["rule_w$i"] || $info["rule_h$i"] || $info["rule_v$i"]){ ?> + <div style="float:right;text-align:right;padding-right:20px;"><a href="#" class="clearrule">(clear)</a></div> + <?php + } ?> + <div class="clear"></div> + </td> + </tr> + <?php + if($i>=25) //Hardcoded limit of 25 rules...also see class.filter.php + break; + } ?> + <tr> + <th colspan="2"> + <em><strong>Filter Actions</strong>: Can be overwriten by other filters depending on processing order. </em> + </th> + </tr> + <tr> + <td width="180"> + Ban Email: + </td> + <td> + <input type="checkbox" name="reject_email" value="1" <?php echo $info['reject_email']?'checked="checked"':''; ?> > + <strong><font class="error">Reject email</font></strong> <em>(All other actions, rules and filters are ignored)</em> + </td> + </tr> + <tr> + <td width="180"> + Reply-To Email: + </td> + <td> + <input type="checkbox" name="use_replyto_email" value="1" <?php echo $info['use_replyto_email']?'checked="checked"':''; ?> > + <strong>Use</strong> Reply-To Email <em>(if available)</em> + </td> + </tr> + <tr> + <td width="180"> + Ticket auto-response: + </td> + <td> + <input type="checkbox" name="disable_autoresponder" value="1" <?php echo $info['disable_autoresponder']?'checked="checked"':''; ?> > + <strong>Disable</strong> auto-response. <em>(Overwrites Dept. settings)</em> + </td> + </tr> + <tr> + <td width="180"> + Department: + </td> + <td> + <select name="dept_id"> + <option value="">— Default —</option> + <?php + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['dept_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Priority: + </td> + <td> + <select name="priority_id"> + <option value="">— Default —</option> + <?php + $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['priority_id']; ?></span> + <em>(Overwrites department's priority)</em> + </td> + </tr> + <tr> + <td width="180"> + SLA Plan: + </td> + <td> + <select name="sla_id"> + <option value="0">— Default —</option> + <?php + $sql='SELECT id,name FROM '.SLA_TABLE.' sla ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['sla_id'] && $id==$info['sla_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error"> <?php echo $errors['sla_id']; ?></span> + <em>(Overwrites department's SLA)</em> + </td> + </tr> + <tr> + <td width="180"> + Auto-assign To: + </td> + <td> + <select name="assign"> + <option value="0">— Unassigned —</option> + + + <?php + + + $sql=' SELECT staff_id,CONCAT_WS(", ",lastname,firstname) as name '. + ' FROM '.STAFF_TABLE.' WHERE isactive=1 ORDER BY name'; + + if(($res=db_query($sql)) && db_num_rows($res)){ + echo '<OPTGROUP label="Staff Members">'; + while (list($id,$name) = db_fetch_row($res)){ + $k="s$id"; + $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':''; + ?> + <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + + <?php } + echo '</OPTGROUP>'; + + } + $sql='SELECT team_id, name FROM '.TEAM_TABLE.' WHERE isenabled=1'; + if(($res=db_query($sql)) && db_num_rows($res)){ + echo '<OPTGROUP label="Teams">'; + while (list($id,$name) = db_fetch_row($res)){ + $k="t$id"; + $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':''; + ?> + <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + <?php + } + echo '</OPTGROUP>'; + } + ?> + </select> + <span class="error"> <?php echo $errors['assign']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="filters.php"'> +</p> +</form> diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..4b9d6f31eb0e04f995ef7ffafde66a6a84bdb618 --- /dev/null +++ b/include/staff/filters.inc.php @@ -0,0 +1,126 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT filter.*,count(rule.id) as rules '. + 'FROM '.EMAIL_FILTER_TABLE.' filter '. + 'LEFT JOIN '.EMAIL_FILTER_RULE_TABLE.' rule ON(rule.filter_id=filter.id) '. + 'GROUP BY filter.id'; +$sortOptions=array('name'=>'filter.name','status'=>'filter.isactive','order'=>'filter.execorder','rules'=>'rules', + 'created'=>'filter.created','updated'=>'filter.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'filter.name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.EMAIL_FILTER_TABLE.' filter '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('filters.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' filters'; +else + $showing='No filters found!'; + +?> + +<div style="width:700;padding-top:5px; float:left;"> + <h2>Email Filters</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="filters.php?a=add" class="Icon newEmailFilter">Add New Filter</a></b></div> +<div class="clear"></div> +<form action="filters.php" method="POST" name="filters" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="320"><a <?php echo $name_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="100"><a <?php echo $status_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="80" style="text-align:center;"><a <?php echo $order_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=order">Order</a></th> + <th width="80" style="text-align:center;"><a <?php echo $rules_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=rules">Rules</a></th> + <th width="120" nowrap><a <?php echo $created_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=created">Date Added</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td> <a href="filters.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a></td> + <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td style="text-align:right;padding-right:25px;"><?php echo $row['execorder']; ?> </td> + <td style="text-align:right;padding-right:25px;"><?php echo $row['rules']; ?> </td> + <td> <?php echo Format::db_date($row['created']); ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['filters'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['filters'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['filters'],true)">Toggle</a> + <?php }else{ + echo 'No filters found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected filters?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected filters?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected filters?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..2591a55af87caf67b56208c27e7c9ceab678b6cc --- /dev/null +++ b/include/staff/footer.inc.php @@ -0,0 +1,16 @@ + </div> + <div id="footer"> + Copyright © 2006-<?php echo date('Y'); ?> osTicket.com. All Rights Reserved. + </div> +<?php +if(is_object($thisstaff) && $thisstaff->isStaff()) { ?> + <div> + <!-- Do not remove <img src="autocron.php" alt="" width="1" height="1" border="0" /> or your auto cron will cease to function --> + <img src="autocron.php" alt="" width="1" height="1" border="0" /> + <!-- Do not remove <img src="autocron.php" alt="" width="1" height="1" border="0" /> or your auto cron will cease to function --> + </div> +<?php +} ?> +</div> +</body> +</html> diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..87921a63cb18aad7a2c73ab73ab5a2c7fb3d8acd --- /dev/null +++ b/include/staff/group.inc.php @@ -0,0 +1,165 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($group && $_REQUEST['a']!='add'){ + $title='Update Group'; + $action='update'; + $submit_text='Save Changes'; + $info=$group->getInfo(); + $info['id']=$group->getId(); + $info['depts']=$info['dept_access']?explode(',',$info['dept_access']):array(); + $qstr.='&id='.$group->getId(); +}else { + $title='Add New Group'; + $action='create'; + $submit_text='Create Group'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; + $info['can_create_tickets']=isset($info['can_create_tickets'])?$info['can_create_tickets']:1; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="groups.php?<?php echo $qstr; ?>" method="post" id="save" name="group"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>User Group</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em><strong>Group Information</strong>: Disabled group will limit staff members access. Admins are exempted.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><strong>Disabled</strong> + <span class="error">* <?php echo $errors['status']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Group Permissions</strong>: Applies to all group members </em> + </th> + </tr> + <tr><td>Can <b>Create</b> Tickets</td> + <td> + <input type="radio" name="can_create_tickets" value="1" <?php echo $info['can_create_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_create_tickets" value="0" <?php echo !$info['can_create_tickets']?'checked="checked"':''; ?> />No + <i>Ability to open tickets on behalf of clients.</i> + </td> + </tr> + <tr><td>Can <b>Edit</b> Tickets</td> + <td> + <input type="radio" name="can_edit_tickets" value="1" <?php echo $info['can_edit_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_edit_tickets" value="0" <?php echo !$info['can_edit_tickets']?'checked="checked"':''; ?> />No + <i>Ability to edit tickets.</i> + </td> + </tr> + <tr><td>Can <b>Close</b> Tickets</td> + <td> + <input type="radio" name="can_close_tickets" value="1" <?php echo $info['can_close_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_close_tickets" value="0" <?php echo !$info['can_close_tickets']?'checked="checked"':''; ?> />No + <i>Ability to close tickets. Staff can still post a response.</i> + </td> + </tr> + <tr><td>Can <b>Assign</b> Tickets</td> + <td> + <input type="radio" name="can_assign_tickets" value="1" <?php echo $info['can_assign_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_assign_tickets" value="0" <?php echo !$info['can_assign_tickets']?'checked="checked"':''; ?> />No + <i>Ability to assign tickets to staff members.</i> + </td> + </tr> + <tr><td>Can <b>Transfer</b> Tickets</td> + <td> + <input type="radio" name="can_transfer_tickets" value="1" <?php echo $info['can_transfer_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_transfer_tickets" value="0" <?php echo !$info['can_transfer_tickets']?'checked="checked"':''; ?> />No + <i>Ability to transfer tickets between departments.</i> + </td> + </tr> + <tr><td>Can <b>Delete</b> Tickets</td> + <td> + <input type="radio" name="can_delete_tickets" value="1" <?php echo $info['can_delete_tickets']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_delete_tickets" value="0" <?php echo !$info['can_delete_tickets']?'checked="checked"':''; ?> />No + <i>Deleted tickets can't be recovered!</i> + </td> + </tr> + <tr><td>Can Ban Emails</td> + <td> + <input type="radio" name="can_ban_emails" value="1" <?php echo $info['can_ban_emails']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_ban_emails" value="0" <?php echo !$info['can_ban_emails']?'checked="checked"':''; ?> />No + <i>Ability to add/remove emails from banlist via ticket interface.</i> + </td> + </tr> + <tr><td>Can Manage Premade</td> + <td> + <input type="radio" name="can_manage_premade" value="1" <?php echo $info['can_manage_premade']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_manage_premade" value="0" <?php echo !$info['can_manage_premade']?'checked="checked"':''; ?> />No + <i>Ability to add/update/disable/delete canned responses and attachments.</i> + </td> + </tr> + <tr><td>Can Manage FAQ</td> + <td> + <input type="radio" name="can_manage_faq" value="1" <?php echo $info['can_manage_faq']?'checked="checked"':''; ?> />Yes + + <input type="radio" name="can_manage_faq" value="0" <?php echo !$info['can_manage_faq']?'checked="checked"':''; ?> />No + <i>Ability to add/update/disable/delete knowledgebase categories and FAQs.</i> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Department Access</strong>: Check all departments the group members are allowed to access. <a href="#" onclick="return select_all(document.forms['group'])">Select All</a> <a href="#" onclick="return reset_all(document.forms['group'])">Select None</a></em> + </th> + </tr> + <?php + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' ORDER BY dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name) = db_fetch_row($res)){ + $ck=($info['depts'] && in_array($id,$info['depts']))?'checked="checked"':''; + echo sprintf('<tr><td colspan=2> <input type="checkbox" name="depts[]" value="%d" %s>%s</td></tr>',$id,$ck,$name); + } + } + ?> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes viewable by all admins. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="groups.php"'> +</p> +</form> diff --git a/include/staff/groups.inc.php b/include/staff/groups.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..15b9b6610916c203cdf25e902228dd8d5281ba5a --- /dev/null +++ b/include/staff/groups.inc.php @@ -0,0 +1,121 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; + +$sql='SELECT grp.*,count(staff.staff_id) as users ' + .' FROM '.GROUP_TABLE.' grp LEFT JOIN '.STAFF_TABLE.' staff USING(group_id) '; +$sql.=' WHERE 1'; +$sortOptions=array('name'=>'grp.group_name','status'=>'grp.group_enabled','users'=>'users','created'=>'grp.created','updated'=>'grp.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'grp.group_name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY grp.group_id ORDER BY $order_by"; +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing="Showing 1-$num of $num groups"; +else + $showing='No groups found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>User Groups</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="groups.php?a=add" class="Icon newgroup">Add New Group</a></b></div> +<div class="clear"></div> +<form action="groups.php" method="POST" name="groups" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7px"> </th> + <th width="250"><a <?php echo $name_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=name">Group Name</a></th> + <th width="80"><a <?php echo $status_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=status">Group Status</a></th> + <th width="80" style="text-align:center;"><a <?php echo $users_sort; ?>href="groups.php?<?php echo $qstr; ?>&sort=users">Members</a></th> + <th width="100"><a <?php echo $created_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=created">Created On</a></th> + <th width="120"><a <?php echo $updated_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)) { + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['group_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['group_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['group_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td><a href="groups.php?id=<?php echo $row['group_id']; ?>"><?php echo $row['group_name']; ?></a> </td> + <td> <?php echo $row['group_enabled']?'Active':'<b>Disabled</b>'; ?></td> + <td style="text-align:right;padding-right:30px"> + <?php if($row['users']>0) { ?> + <a href="staff.php?gid=<?php echo $row['group_id']; ?>"><?php echo $row['users']; ?></a> + <?php }else{ ?> 0 + <?php } ?> + + </td> + <td><?php echo Format::db_date($row['created']); ?> </td> + <td><?php echo Format::db_datetime($row['updated']); ?> </td> + </tr> + <?php + } //end of while. + } ?> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['groups'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['groups'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['groups'],true)">Toggle</a> + <?php }else{ + echo 'No groups found!'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected groups?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected groups?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected groups?");'> +</p> +<?php +endif; +?> + +</form> + diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..590673c1616c4417c3793df67c9c2234add07566 --- /dev/null +++ b/include/staff/header.inc.php @@ -0,0 +1,79 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <meta charset="utf-8"> + <title>osTicket Staff Control Panel</title> + <!--[if IE]> + <style type="text/css"> + .tip_shadow { display:block !important; } + </style> + <![endif]--> + <script type="text/javascript" src="../js/jquery.min.js"></script> + <script type="text/javascript" src="./js/calendar.js"></script> + <script type="text/javascript" src="./js/tips.js"></script> + <script type="text/javascript" src="./js/nicEdit.js"></script> + <script type="text/javascript" src="./js/scp.js"></script> + <link rel="stylesheet" href="./css/scp.css" media="screen"> +</head> +<body> +<div id="container"> + <div id="header"> + <a href="index.php" id="logo">osTicket - Customer Support System</a> + <p id="info">Welcome back, <strong><?php echo $thisstaff->getUsername(); ?></strong> + <?php + if($thisstaff->isAdmin() && !defined('ADMINPAGE')) { ?> + | <a href="admin.php">Admin Panel</a> + <?php }else{ ?> + | <a href="index.php">Staff Panel</a> + <?php } ?> + | <a href="profile.php">My Preferences</a> | <a href="logout.php">Log Out</a> + </p> + </div> + <ul id="nav"> + <?php + if(($tabs=$nav->getTabs()) && is_array($tabs)){ + foreach($tabs as $name =>$tab) { + echo sprintf('<li class="%s"><a href="%s">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']); + if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){ + echo "<ul>\n"; + foreach($subnav as $item) { + echo sprintf('<li><a class="%s" href="%s" title="%s" >%s</a></li>', + $item['iconclass'],$item['href'],$item['title'],$item['desc']); + } + echo "\n</ul>\n"; + } + echo "\n</li>\n"; + } + } ?> + </ul> + <ul id="sub_nav"> + <?php + if(($subnav=$nav->getSubMenu()) && is_array($subnav)){ + $activeMenu=$nav->getActiveMenu(); + if($activeMenu>0 && !isset($subnav[$activeMenu-1])) + $activeMenu=0; + foreach($subnav as $k=> $item) { + if($item['droponly']) continue; + $class=$item['iconclass']; + if ($activeMenu && $k+1==$activeMenu + or (!$activeMenu + && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false + or ($item['urls'] + && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls']) + ) + ))) + $class="$class active"; + + echo sprintf('<li><a class="%s" href="%s" title="%s" >%s</a></li>',$class,$item['href'],$item['title'],$item['desc']); + } + } + ?> + </ul> + <div id="content"> + <?php if($errors['err']) { ?> + <div id="msg_error"><?php echo $errors['err']; ?></div> + <?php }elseif($msg) { ?> + <div id="msg_notice"><?php echo $msg; ?></div> + <?php }elseif($warn) { ?> + <div id="msg_warning"><?php echo $warn; ?></div> + <?php } ?> diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..ad9f808bcfd6afedfabec510dda920ca2abaae0a --- /dev/null +++ b/include/staff/helptopic.inc.php @@ -0,0 +1,198 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($topic && $_REQUEST['a']!='add'){ + $title='Update Help Topic'; + $action='update'; + $submit_text='Save Changes'; + $info=$topic->getInfo(); + $info['id']=$topic->getId(); + $qstr.='&id='.$topic->getId(); +}else { + $title='Add New Help Topic'; + $action='create'; + $submit_text='Add Topic'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; + $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="helptopics.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Help Topic</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Help Topic Information</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Topic: + </td> + <td> + <input type="text" size="30" name="topic" value="<?php echo $info['topic']; ?>"> + <span class="error">* <?php echo $errors['topic']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>>Active + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Type: + </td> + <td> + <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>>Public + <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>>Private <em>(Internal)</em> + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Priority: + </td> + <td> + <select name="priority_id"> + <option value="">— Select Priority —</option> + <?php + $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['priority_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Department: + </td> + <td> + <select name="dept_id"> + <option value="">— Select Department —</option> + <?php + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['dept_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + SLA Plan: + </td> + <td> + <select name="sla_id"> + <option value="0">— Department's Default —</option> + <?php + $sql='SELECT id,name FROM '.SLA_TABLE.' sla ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['sla_id'] && $id==$info['sla_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error"> <?php echo $errors['sla_id']; ?></span> + <em>(Overwrites department's SLA)</em> + </td> + </tr> + <tr> + <td width="180"> + Auto-assign To: + </td> + <td> + <select name="assign"> + <option value="0">— Unassigned —</option> + + + <?php + + + $sql=' SELECT staff_id,CONCAT_WS(", ",lastname,firstname) as name '. + ' FROM '.STAFF_TABLE.' WHERE isactive=1 ORDER BY name'; + + if(($res=db_query($sql)) && db_num_rows($res)){ + echo '<OPTGROUP label="Staff Members">'; + while (list($id,$name) = db_fetch_row($res)){ + $k="s$id"; + $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':''; + ?> + <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + + <?php } + echo '</OPTGROUP>'; + + } + $sql='SELECT team_id, name FROM '.TEAM_TABLE.' WHERE isenabled=1'; + if(($res=db_query($sql)) && db_num_rows($res)){ + echo '<OPTGROUP label="Teams">'; + while (list($id,$name) = db_fetch_row($res)){ + $k="t$id"; + $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':''; + ?> + <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + <?php + } + echo '</OPTGROUP>'; + } + ?> + </select> + <span class="error"> <?php echo $errors['assign']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Ticket auto-response: + </td> + <td> + <input type="checkbox" name="noautoresp" value="1" <?php echo $info['noautoresp']?'checked="checked"':''; ?> > + <strong>Disable</strong> new ticket auto-response for this topic (Overwrites Dept. settings). + </td> + </tr> + + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes about the help topic. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="helptopics.php"'> +</p> +</form> diff --git a/include/staff/helptopics.inc.php b/include/staff/helptopics.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..51a7666889f562676910351c69edcf8e10f96b91 --- /dev/null +++ b/include/staff/helptopics.inc.php @@ -0,0 +1,129 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT topic.*,dept.dept_name as department,priority_desc as priority '. + ' FROM '.TOPIC_TABLE.' topic '. + ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=topic.dept_id) '. + ' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (pri.priority_id=topic.priority_id) '; +$sql.=' WHERE 1'; +$sortOptions=array('name'=>'topic.topic','status'=>'topic.isactive','type'=>'topic.ispublic', + 'dept'=>'department','priority'=>'priority','updated'=>'topic.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'topic.topic'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.TOPIC_TABLE.' topic '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('helptopics.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY topic.topic_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' help topics'; +else + $showing='No help topic found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>Help Topics</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="helptopics.php?a=add" class="Icon newHelpTopic">Add New Help Topic</a></b></div> +<div class="clear"></div> +<form action="helptopics.php" method="POST" name="topics" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="320"><a <?php echo $name_sort; ?> href="helptopics.php?<?php echo $qstr; ?>&sort=name">Help Topic</a></th> + <th width="80"><a <?php echo $status_sort; ?> href="helptopics.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="100"><a <?php echo $type_sort; ?> href="helptopics.php?<?php echo $qstr; ?>&sort=type">Type</a></th> + <th width="100"><a <?php echo $priority_sort; ?> href="helptopics.php?<?php echo $qstr; ?>&sort=priority">Priority</a></th> + <th width="200"><a <?php echo $dept_sort; ?> href="helptopics.php?<?php echo $qstr; ?>&sort=dept">Department</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="helptopics.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultId=$cfg->getDefaultDeptId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['topic_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['topic_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['topic_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> + onClick="highLight(this.value,this.checked);"> </td> + <td><a href="helptopics.php?id=<?php echo $row['topic_id']; ?>"><?php echo $row['topic']; ?></a> </td> + <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td><?php echo $row['ispublic']?'Public':'<b>Private</b>'; ?></td> + <td><?php echo $row['priority']; ?></td> + <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['department']; ?></a></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['topics'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['topics'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['topics'],true)">Toggle</a> + <?php }else{ + echo 'No help topics found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected help topics?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected help topics?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected help topics?");'> +</p> +<?php +endif; +?> + +</form> + diff --git a/include/staff/index.php b/include/staff/index.php new file mode 100644 index 0000000000000000000000000000000000000000..f9518a67ebc4a50b4c9b59f7920221c245a6b0dc --- /dev/null +++ b/include/staff/index.php @@ -0,0 +1,3 @@ +<?php +header('Location: ../'); +?> diff --git a/include/staff/kb-categories.inc.php b/include/staff/kb-categories.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0ab8b03fc1e6326eac435768954c049dc0b3aa64 --- /dev/null +++ b/include/staff/kb-categories.inc.php @@ -0,0 +1,114 @@ +<?php +if(!defined('OSTSTAFFINC') || !$thisstaff) die('Access Denied'); + +?> +<h2>Frequently Asked Questions</h2> +<form id="kbSearch" action="kb.php" method="get" style="padding-top:15px;"> + <input type="hidden" name="a" value="search"> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="440"> + <input id="query" type="text" size="20" name="q" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>"> + <select name="cid"> + <option value="">— All Categories —</option> + <?php + $sql='SELECT category_id, name, count(faq.category_id) as faqs ' + .' FROM '.FAQ_CATEGORY_TABLE.' cat ' + .' LEFT JOIN '.FAQ_TABLE.' faq USING(category_id) ' + .' GROUP BY cat.category_id ' + .' HAVING faqs>0 ' + .' ORDER BY cat.name DESC '; + if(($res=db_query($sql)) && db_num_rows($res)) { + while($row=db_fetch_array($res)) + echo sprintf('<option value="%d" %s>%s (%d)</option>', + $row['category_id'], + ($_REQUEST['cid'] && $row['category_id']==$_REQUEST['cid']?'selected="selected"':''), + $row['name'], + $row['faqs']); + } + ?> + </select> + </td> + <td width="100" rowspan="2"> + <input id="searchSubmit" type="submit" value="Search"> + </td> + </tr> + <tr> + <td width="400"> + <select name="topicId" style="width:350px;"> + <option value="">— All Help Topics —</option> + <?php + $sql='SELECT ht.topic_id, ht.topic, count(faq.topic_id) as faqs ' + .' FROM '.TOPIC_TABLE.' ht ' + .' LEFT JOIN '.FAQ_TOPIC_TABLE.' faq USING(topic_id) ' + .' GROUP BY ht.topic_id ' + .' HAVING faqs>0 ' + .' ORDER BY ht.topic DESC '; + if(($res=db_query($sql)) && db_num_rows($res)) { + while($row=db_fetch_array($res)) + echo sprintf('<option value="%d" %s>%s (%d)</option>', + $row['topic_id'], + ($_REQUEST['topicId'] && $row['topic_id']==$_REQUEST['cid']?'selected="selected"':''), + $row['topic'], $row['faqs']); + } + ?> + </select> + </td> + </tr> + </table> +</form> +<hr> +<div> +<?php +if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. + $sql='SELECT faq.faq_id, question, ispublished, count(attach.file_id) as attachments ' + .' FROM '.FAQ_TABLE.' faq ' + .' LEFT JOIN '.FAQ_ATTACHMENT_TABLE.' attach ON(attach.faq_id=faq.faq_id) ' + .' WHERE 1 '; + if($_REQUEST['cid']) + $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']); + + if($_REQUEST['q']) + $sql.=" AND MATCH(question,answer,keywords) AGAINST ('".db_input($_REQUEST['q'],false)."')"; + + $sql.=' GROUP BY faq.faq_id'; + echo "<div><strong>Search Results</strong></div><div class='clear'></div>"; + if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<div id="faq"> + <ol>'; + while($row=db_fetch_array($res)) { + echo sprintf(' + <li><a href="faq.php?id=%d" class="previewfaq">%s</a> - <span>%s</span></li>', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo ' </ol> + </div>'; + } else { + echo '<strong class="faded">The search did not match any FAQs.</strong>'; + } +} else { //Category Listing. + $sql='SELECT cat.category_id, cat.name, cat.description, cat.ispublic, count(faq.faq_id) as faqs ' + .' FROM '.FAQ_CATEGORY_TABLE.' cat ' + .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) ' + .' GROUP BY cat.category_id ' + .' ORDER BY cat.name'; + if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<div>Click on the category to browse FAQs.</div> + <ul id="kb">'; + while($row=db_fetch_array($res)) { + + echo sprintf(' + <li> + <h4><a href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></h4> + %s + </li>',$row['category_id'],$row['name'],$row['faqs'], + ($row['ispublic']?'Public':'Internal'), + Format::safe_html($row['description'])); + } + echo '</ul>'; + } else { + echo 'NO FAQs found'; + } +} +?> +</div> diff --git a/include/staff/kb-category.inc.php b/include/staff/kb-category.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..65c428a2f703f0b58332bbd416426c50cec765e7 --- /dev/null +++ b/include/staff/kb-category.inc.php @@ -0,0 +1,50 @@ +<?php +if(!defined('OSTSTAFFINC') || !$category || !$thisstaff) die('Access Denied'); + +?> +<div style="width:700;padding-top:10px; float:left;"> + <h2>Frequently Asked Questions</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> </div> +<div class="clear"></div> +<br> +<div> + <strong><?php echo $category->getName() ?></strong> + <span>(<?php echo $category->isPublic()?'Public':'Internal'; ?>)</span> + <br> + <div class="faded"> Last updated <?php echo Format::db_daydatetime($category->getUpdateDate()); ?></div> +</div> +<p> +<?php echo Format::safe_html($category->getDescription()); ?> +</p> +<?php +if($thisstaff->canManageFAQ()) { + echo sprintf('<a href="categories.php?id=%d" class="Icon newHelpTopic">Edit Category</a> + | <a href="categories.php" class="Icon newHelpTopic">Delete Category</a> + | <a href="faq.php?cid=%d&a=add" class="Icon newHelpTopic">Add New FAQ</a>', + $category->getId(), + $category->getId()); +} +?> +<hr> +<?php + +$sql='SELECT faq.faq_id, question, ispublished, count(attach.file_id) as attachments ' + .' FROM '.FAQ_TABLE.' faq ' + .' LEFT JOIN '.FAQ_ATTACHMENT_TABLE.' attach ON(attach.faq_id=faq.faq_id) ' + .' WHERE faq.category_id='.db_input($category->getId()) + .' GROUP BY faq.faq_id'; +if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<div id="faq"> + <ol>'; + while($row=db_fetch_array($res)) { + echo sprintf(' + <li><a href="faq.php?id=%d" class="previewfaq">%s</a> - <span>%s</span></li>', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo ' </ol> + </div>'; +}else { + echo '<strong>Category does not have FAQs</strong>'; +} +?> diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php new file mode 100644 index 0000000000000000000000000000000000000000..d2b94d5516e30ae4e447d328f72f468455ca3b17 --- /dev/null +++ b/include/staff/login.tpl.php @@ -0,0 +1,27 @@ +<?php defined('OSTSCPINC') or die('Invalid path'); ?> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<title>osTicket:: SCP Login</title> +<link rel="stylesheet" href="css/login.css" type="text/css" /> +<meta name="robots" content="noindex" /> +<meta http-equiv="cache-control" content="no-cache" /> +<meta http-equiv="pragma" content="no-cache" /> +</head> +<body id="loginBody"> +<div id="loginBox"> + <h1 id="logo"><a href="index.php">osTicket Staff Control Panel</a></h1> + <h1><?php echo Format::htmlchars($msg); ?></h1> + <br /> + <form action="login.php" method="post"> + <input type="hidden" name=do value="scplogin" /> + <table border=0 align="center"> + <tr><td width=100px align="right"><b>Username</b>:</td><td><input type="text" name="username" id="name" value="" /></td></tr> + <tr><td align="right"><b>Password</b>:</td><td><input type="password" name="passwd" id="pass" /></td></tr> + <tr><td> </td><td> <input class="submit" type="submit" name="submit" value="Login" /></td></tr> + </table> +</form> +</div> +<div id="copyRights">Copyright © <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div> +</body> +</html> diff --git a/include/staff/newticket.inc.php b/include/staff/newticket.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..42afb7c3e07ac579516750ac60b9826a19d0eb4e --- /dev/null +++ b/include/staff/newticket.inc.php @@ -0,0 +1,224 @@ +<?php +if(!defined('OSTSCPINC') || !is_object($thisstaff) || !$thisstaff->isStaff()) die('Access Denied'); +$info=($_POST && $errors)?Format::input($_POST):array(); //on error...use the post data +?> +<div width="100%"> + <?php if($errors['err']) { ?> + <p align="center" id="errormessage"><?php echo $errors['err']; ?></p> + <?php }elseif($msg) { ?> + <p align="center" class="infomessage"><?php echo $msg; ?></p> + <?php }elseif($warn) { ?> + <p class="warnmessage"><?php echo $warn; ?></p> + <?php } ?> +</div> +<table width="80%" border="0" cellspacing=1 cellpadding=2> + <form action="tickets.php" method="post" enctype="multipart/form-data"> + <input type='hidden' name='a' value='open'> + <tr><td align="left" colspan=2>Please fill in the form below to open a new ticket.</td></tr> + <tr> + <td align="left" nowrap width="20%"><b>Email Address:</b></td> + <td> + <input type="text" id="email" name="email" size="25" value="<?php echo $info['email']; ?>"> + <font class="error"><b>*</b> <?php echo $errors['email']; ?></font> + <?php if($cfg->notifyONNewStaffTicket()) { ?> + + <input type="checkbox" name="alertuser" <?php echo (!$errors || $info['alertuser'])? 'checked': ''; ?>>Send alert to user. + <?php } ?> + </td> + </tr> + <tr> + <td align="left" ><b>Full Name:</b></td> + <td> + <input type="text" id="name" name="name" size="25" value="<?php echo $info['name']; ?>"> + <font class="error"><b>*</b> <?php echo $errors['name']; ?></font> + </td> + </tr> + <tr> + <td align="left">Telephone:</td> + <td><input type="text" name="phone" size="25" value="<?php echo $info['phone']; ?>"> + Ext <input type="text" name="phone_ext" size="6" value="<?php echo $info['phone_ext']; ?>"> + <font class="error"> <?php echo $errors['phone']; ?></font></td> + </tr> + <tr height=2px><td align="left" colspan=2 > </td</tr> + <tr> + <td align="left"><b>Ticket Source:</b></td> + <td> + <select name="source"> + <option value="" selected >Select Source</option> + <option value="Phone" <?php echo ($info['source']=='Phone')?'selected':''; ?>>Phone</option> + <option value="Email" <?php echo ($info['source']=='Email')?'selected':''; ?>>Email</option> + <option value="Other" <?php echo ($info['source']=='Other')?'selected':''; ?>>Other</option> + </select> + <font class="error"><b>*</b> <?php echo $errors['source']; ?></font> + </td> + </tr> + <tr> + <td align="left"><b>Department:</b></td> + <td> + <select name="deptId"> + <option value="" selected >Select Department</option> + <?php + $services= db_query('SELECT dept_id,dept_name FROM '.DEPT_TABLE.' ORDER BY dept_name'); + while (list($deptId,$dept) = db_fetch_row($services)){ + $selected = ($info['deptId']==$deptId)?'selected':''; ?> + <option value="<?php echo $deptId; ?>"<?php echo $selected; ?>><?php echo $dept; ?></option> + <?php + } ?> + </select> + <font class="error"><b>*</b> <?php echo $errors['deptId']; ?></font> + </td> + </tr> + <tr> + <td align="left"><b>Subject:</b></td> + <td> + <input type="text" name="subject" size="35" value="<?php echo $info['subject']; ?>"> + <font class="error">* <?php echo $errors['subject']; ?></font> + </td> + </tr> + <tr> + <td align="left" valign="top"><b>Issue Summary:</b></td> + <td> + <i>Visible to client/customer.</i><font class="error"><b>* <?php echo $errors['issue']; ?></b></font><br/> + <?php + $sql='SELECT canned_id,title FROM '.CANNED_TABLE.' WHERE isenabled=1'; + + if(($res=db_query($sql)) && db_num_rows($res)) { + ?> + Premade: + <select id="canned" name="canned" + onChange="getCannedResponse(this.options[this.selectedIndex].value,this.form,'issue');this.selectedIndex='0';" > + <option value="0" selected="selected">Select a premade reply/issue</option> + <?php while(list($cannedId,$title)=db_fetch_row($res)) { ?> + <option value="<?php echo $cannedId; ?>" ><?php echo Format::htmlchars($title); ?></option> + <?php } ?> + </select> <label><input type='checkbox' value='1' name=append checked="true" />Append</label> + <?php } ?> + <textarea name="issue" cols="55" rows="8" wrap="soft"><?php echo $info['issue']; ?></textarea></td> + </tr> + <?php if($cfg->canUploadFiles()) { + ?> + <tr> + <td>Attachment:</td> + <td> + <input type="file" name="attachment"><font class="error"> <?php echo $errors['attachment']; ?></font> + </td> + </tr> + <?php } ?> + <tr> + <td align="left" valign="top">Internal Note:</td> + <td> + <i>Optional Internal note(s).</i><font class="error"><b> <?php echo $errors['note']; ?></b></font><br/> + <textarea name="note" cols="55" rows="5" wrap="soft"><?php echo $info['note']; ?></textarea></td> + </tr> + + <tr> + <td align="left" valign="top">Due Date:</td> + <td> + <i>Time is based on your time zone (GM <?php echo $thisstaff->getTZoffset(); ?>)</i> <font class="error"> <?php echo $errors['time']; ?></font><br> + <input id="duedate" name="duedate" value="<?php echo Format::htmlchars($info['duedate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF> + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('duedate')); return false;"><img src='images/cal.png'border=0 alt=""></a> + + <?php + $min=$hr=null; + if($info['time']) + list($hr,$min)=explode(':',$info['time']); + echo Misc::timeDropdown($hr,$min,'time'); + ?> + <font class="error"> <?php echo $errors['duedate']; ?></font> + </td> + </tr> + <?php + $sql='SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE.' ORDER BY priority_urgency DESC'; + if(($priorities=db_query($sql)) && db_num_rows($priorities)){ ?> + <tr> + <td align="left">Priority:</td> + <td> + <select name="pri"> + <?php + $info['pri']=$info['pri']?$info['pri']:$cfg->getDefaultPriorityId(); + while($row=db_fetch_array($priorities)){ ?> + <option value="<?php echo $row['priority_id']; ?>" <?php echo $info['pri']==$row['priority_id']?'selected':''; ?> ><?php echo $row['priority_desc']; ?></option> + <?php } ?> + </select> + </td> + </tr> + <?php } ?> + <?php + $services= db_query('SELECT topic_id,topic FROM '.TOPIC_TABLE.' WHERE isactive=1 ORDER BY topic'); + if($services && db_num_rows($services)){ ?> + <tr> + <td align="left" valign="top">Help Topic:</td> + <td> + <select name="topicId"> + <option value="" selected >Select One</option> + <?php + while (list($topicId,$topic) = db_fetch_row($services)){ + $selected = ($info['topicId']==$topicId)?'selected':''; ?> + <option value="<?php echo $topicId; ?>"<?php echo $selected; ?>><?php echo $topic; ?></option> + <?php + } ?> + </select> + <font class="error"> <?php echo $errors['topicId']; ?></font> + </td> + </tr> + <?php + } ?> + <tr> + <td>Assign To:</td> + <td> + <select id="staffId" name="staffId"> + <option value="0" selected="selected">-Assign To Staff-</option> + <?php + //TODO: make sure the user's group is also active....DO a join. + $sql=' SELECT staff_id,CONCAT_WS(", ",lastname,firstname) as name FROM '.STAFF_TABLE.' WHERE isactive=1 AND onvacation=0 '; + $depts= db_query($sql.' ORDER BY lastname,firstname '); + while (list($staffId,$staffName) = db_fetch_row($depts)){ + $selected = ($info['staffId']==$staffId)?'selected':''; ?> + <option value="<?php echo $staffId; ?>"<?php echo $selected; ?>><?php echo $staffName; ?></option> + <?php + } ?> + </select><font class='error'> <?php echo $errors['staffId']; ?></font> + + <input type="checkbox" name="alertstaff" <?php echo (!$errors || $info['alertstaff'])? 'checked': ''; ?>>Send alert to assigned staff. + </td> + </tr> + <tr> + <td>Signature:</td> + <td> <?php + $appendStaffSig=$thisstaff->appendMySignature(); + $info['signature']=!$info['signature']?'none':$info['signature']; //change 'none' to 'mine' to default to staff signature. + ?> + <div style="margin-top: 2px;"> + <label><input type="radio" name="signature" value="none" checked > None</label> + <?php if($appendStaffSig) { ?> + <label> <input type="radio" name="signature" value="mine" <?php echo $info['signature']=='mine'?'checked':''; ?> > My signature</label> + <?php } ?> + <label><input type="radio" name="signature" value="dept" <?php echo $info['signature']=='dept'?'checked':''; ?> > Dept Signature (if any)</label> + </div> + </td> + </tr> + <tr height=2px><td align="left" colspan=2 > </td</tr> + <tr> + <td></td> + <td> + <input class="button" type="submit" name="submit_x" value="Submit Ticket"> + <input class="button" type="reset" value="Reset"> + <input class="button" type="button" name="cancel" value="Cancel" onClick='window.location.href="tickets.php"'> + </td> + </tr> + </form> +</table> +<script type="text/javascript"> + + var options = { + script:"ajax.php?api=tickets&f=searchbyemail&limit=10&", + varname:"input", + json: true, + shownoresults:false, + maxresults:10, + callback: function (obj) { document.getElementById('email').value = obj.id; document.getElementById('name').value = obj.info; return false;} + }; + var autosug = new bsn.AutoSuggest('email', options); +</script> + diff --git a/include/staff/preference.inc.php b/include/staff/preference.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..d956d011dff2c15ad87c7e023f69303a87034353 --- /dev/null +++ b/include/staff/preference.inc.php @@ -0,0 +1,498 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +//Get the config info. +$config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfig()); +//Basic checks for warnings... +$warn=array(); +if($config['allow_attachments'] && !$config['upload_dir']) { + $errors['allow_attachments']='You need to setup upload dir.'; +}else{ + if(!$config['allow_attachments'] && $config['allow_email_attachments']) + $warn['allow_email_attachments']='*Attachments Disabled.'; + if(!$config['allow_attachments'] && ($config['allow_online_attachments'] or $config['allow_online_attachments_onlogin'])) + $warn['allow_online_attachments']='<br>*Attachments Disabled.'; +} + +if(!$errors['enable_captcha'] && $config['enable_captcha'] && !extension_loaded('gd')) + $errors['enable_captcha']='GD required for captcha to work'; + + +//Not showing err on post to avoid alarming the user...after an update. +if(!$errors['err'] &&!$msg && $warn ) + $errors['err']='Possible errors detected, please check the warnings below'; + +$gmtime=Misc::gmtime(); +$depts= db_query('SELECT dept_id,dept_name FROM '.DEPT_TABLE.' WHERE ispublic=1'); +$templates=db_query('SELECT tpl_id,name FROM '.EMAIL_TEMPLATE_TABLE.' WHERE cfg_id='.db_input($cfg->getId())); +?> +<div class="msg">System Preferences and Settings (v<?php echo $config['ostversion']; ?>)</div> +<table width="100%" border="0" cellspacing=0 cellpadding=0> + <form action="admin.php?t=pref" method="post"> + <input type="hidden" name="t" value="pref"> + <tr><td> + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header" ><td colspan=2>General Settings</td></tr> + <tr class="subheader"> + <td colspan=2">Offline mode will disable client interface and <b>only</b> allow <b>super admins</b> to login to Staff Control Panel</td> + </tr> + <tr><th><b>Helpdesk Status</b></th> + <td> + <input type="radio" name="isonline" value="1" <?php echo $config['isonline']?'checked':''; ?> /><b>Online</b> (Active) + <input type="radio" name="isonline" value="0" <?php echo !$config['isonline']?'checked':''; ?> /><b>Offline</b> (Disabled) + <font class="warn"> <?php echo $config['isoffline']?'osTicket offline':''; ?></font> + </td> + </tr> + <tr><th>Helpdesk URL:</th> + <td> + <input type="text" size="40" name="helpdesk_url" value="<?php echo $config['helpdesk_url']; ?>"> + <font class="error">* <?php echo $errors['helpdesk_url']; ?></font></td> + </tr> + <tr><th>Helpdesk Name/Title:</th> + <td><input type="text" size="40" name="helpdesk_title" value="<?php echo $config['helpdesk_title']; ?>"> </td> + </tr> + <tr><th>Default Email Templates:</th> + <td> + <select name="default_template_id"> + <option value=0>Select Default Template</option> + <?php + while (list($id,$name) = db_fetch_row($templates)){ + $selected = ($config['default_template_id']==$id)?'SELECTED':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + <?php + } ?> + </select> <font class="error">* <?php echo $errors['default_template_id']; ?></font> + </td> + </tr> + <tr><th>Default Department:</th> + <td> + <select name="default_dept_id"> + <option value=0>Select Default Dept</option> + <?php + while (list($id,$name) = db_fetch_row($depts)){ + $selected = ($config['default_dept_id']==$id)?'SELECTED':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?> Dept</option> + <?php + } ?> + </select> <font class="error">* <?php echo $errors['default_dept_id']; ?></font> + </td> + </tr> + <tr><th>Default Page Size:</th> + <td> + <select name="max_page_size"> + <?php + $pagelimit=$config['max_page_size']; + for ($i = 5; $i <= 50; $i += 5) { + ?> + <option <?php echo $config['max_page_size'] == $i ? 'SELECTED':''; ?> value="<?php echo $i; ?>"><?php echo $i; ?></option> + <?php + } ?> + </select> + </td> + </tr> + <tr><th>System Log Level:</th> + <td> + <select name="log_level"> + <option value=0 <?php echo $config['log_level'] == 0 ? 'selected="selected"':''; ?>>None (Disable Logger)</option> + <option value=3 <?php echo $config['log_level'] == 3 ? 'selected="selected"':''; ?>> DEBUG</option> + <option value=2 <?php echo $config['log_level'] == 2 ? 'selected="selected"':''; ?>> WARN</option> + <option value=1 <?php echo $config['log_level'] == 1 ? 'selected="selected"':''; ?>> ERROR</option> + </select> + Purge logs after + <select name="log_graceperiod"> + <option value=0 selected> None (Disable)</option> + <?php + for ($i = 1; $i <=12; $i++) { + ?> + <option <?php echo $config['log_graceperiod'] == $i ? 'SELECTED':''; ?> value="<?php echo $i; ?>"><?php echo $i; ?> <?php echo ($i>1)?'Months':'Month'; ?></option> + <?php + } ?> + </select> + </td> + </tr> + <tr><th>Staff Excessive Logins:</th> + <td> + <select name="staff_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_max_logins']==$i)?'selected="selected"':''),$i); + } + ?> + </select> attempt(s) allowed + before a + <select name="staff_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_login_timeout']==$i)?'selected="selected"':''),$i); + } + ?> + </select> min. timeout (penalty in minutes) + </td> + </tr> + <tr><th>Staff Session Timeout:</th> + <td> + <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>"> + (<i>Staff's max Idle time in minutes. Enter 0 to disable timeout</i>) + </td> + </tr> + <tr><th>Bind Staff Session to IP:</th> + <td> + <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked':''; ?>> + Bind staff's session to login IP. + </td> + </tr> + + <tr><th>Client Excessive Logins:</th> + <td> + <select name="client_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_max_logins']==$i)?'selected="selected"':''),$i); + } + + ?> + </select> attempt(s) allowed + before a + <select name="client_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_login_timeout']==$i)?'selected="selected"':''),$i); + } + ?> + </select> min. timeout (penalty in minutes) + </td> + </tr> + + <tr><th>Client Session Timeout:</th> + <td> + <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>"> + (<i>Client's max Idle time in minutes. Enter 0 to disable timeout</i>) + </td> + </tr> + <tr><th>Clickable URLs:</th> + <td> + <input type="checkbox" name="clickable_urls" <?php echo $config['clickable_urls']?'checked':''; ?>> + Make URLs clickable + </td> + </tr> + <tr><th>Enable Auto Cron:</th> + <td> + <input type="checkbox" name="enable_auto_cron" <?php echo $config['enable_auto_cron']?'checked':''; ?>> + Enable cron call on staff's activity + </td> + </tr> + </table> + + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2>Date & Time</td></tr> + <tr class="subheader"> + <td colspan=2>Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</td> + </tr> + <tr><th>Time Format:</th> + <td> + <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>"> + <font class="error">* <?php echo $errors['time_format']; ?></font> + <i><?php echo Format::date($config['time_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></i></td> + </tr> + <tr><th>Date Format:</th> + <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>"> + <font class="error">* <?php echo $errors['date_format']; ?></font> + <i><?php echo Format::date($config['date_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></i> + </td> + </tr> + <tr><th>Date & Time Format:</th> + <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>"> + <font class="error">* <?php echo $errors['datetime_format']; ?></font> + <i><?php echo Format::date($config['datetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></i> + </td> + </tr> + <tr><th>Day, Date & Time Format:</th> + <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>"> + <font class="error">* <?php echo $errors['daydatetime_format']; ?></font> + <i><?php echo Format::date($config['daydatetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></i> + </td> + </tr> + <tr><th>Default Timezone:</th> + <td> + <select name="timezone_offset"> + <?php + $gmoffset = date("Z") / 3600; //Server's offset. + echo"<option value=\"$gmoffset\">Server Time (GMT $gmoffset:00)</option>"; //Default if all fails. + $timezones= db_query('SELECT offset,timezone FROM '.TIMEZONE_TABLE); + while (list($offset,$tz) = db_fetch_row($timezones)){ + $selected = ($config['timezone_offset'] ==$offset) ?'SELECTED':''; + $tag=($offset)?"GMT $offset ($tz)":" GMT ($tz)"; + ?> + <option value="<?php echo $offset; ?>"<?php echo $selected; ?>><?php echo $tag; ?></option> + <?php + } ?> + </select> + </td> + </tr> + <tr> + <th>Daylight Saving:</th> + <td> + <input type="checkbox" name="enable_daylight_saving" <?php echo $config['enable_daylight_saving'] ? 'checked': ''; ?>>Observe daylight savings + </td> + </tr> + </table> + + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2>Ticket Options & Settings</td></tr> + <tr class="subheader"><td colspan=2>If enabled ticket lock get auto-renewed on form activity.</td></tr> + <tr><th valign="top">Ticket IDs:</th> + <td> + <input type="radio" name="random_ticket_ids" value="0" <?php echo !$config['random_ticket_ids']?'checked':''; ?> /> Sequential + <input type="radio" name="random_ticket_ids" value="1" <?php echo $config['random_ticket_ids']?'checked':''; ?> />Random (recommended) + </td> + </tr> + <tr><th valign="top">Ticket Priority:</th> + <td> + <select name="default_priority_id"> + <?php + $priorities= db_query('SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE); + while (list($id,$tag) = db_fetch_row($priorities)){ ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_priority_id']==$id)?'selected':''; ?>><?php echo $tag; ?></option> + <?php + } ?> + </select> Default priority<br/> + <input type="checkbox" name="allow_priority_change" <?php echo $config['allow_priority_change'] ?'checked':''; ?>> + Allow user to overwrite/set priority (new web tickets)<br/> + <input type="checkbox" name="use_email_priority" <?php echo $config['use_email_priority'] ?'checked':''; ?> > + Use email priority when available (new emailed tickets) + + </td> + </tr> + <tr><th>Maximum <b>Open</b> Tickets:</th> + <td> + <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>"> + per email. (<i>Helps with spam and flood control. Enter 0 for unlimited</i>) + </td> + </tr> + <tr><th>Auto-Lock Time:</td> + <td> + <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>"> + <font class="error"><?php echo $errors['autolock_minutes']; ?></font> + (<i>Minutes to lock a ticket on activity. Enter 0 to disable locking</i>) + </td> + </tr> + <tr><th>Ticket Grace Period:</th> + <td> + <input type="text" name="overdue_grace_period" size=4 value="<?php echo $config['overdue_grace_period']; ?>"> + (<i>Hours before ticket is marked overdue. Enter 0 to disable aging.</i>) + </td> + </tr> + <tr><th>Reopened Tickets:</th> + <td> + <input type="checkbox" name="auto_assign_reopened_tickets" <?php echo $config['auto_assign_reopened_tickets'] ? 'checked': ''; ?>> + Auto-assign reopened tickets to last respondent 'available'. (<i> 3 months limit</i>) + </td> + </tr> + <tr><th>Assigned Tickets:</th> + <td> + <input type="checkbox" name="show_assigned_tickets" <?php echo $config['show_assigned_tickets']?'checked':''; ?>> + Show assigned tickets on open queue. + </td> + </tr> + <tr><th>Answered Tickets:</th> + <td> + <input type="checkbox" name="show_answered_tickets" <?php echo $config['show_answered_tickets']?'checked':''; ?>> + Show answered tickets on open queue. + </td> + </tr> + <tr><th>Ticket Activity Log:</th> + <td> + <input type="checkbox" name="log_ticket_activity" <?php echo $config['log_ticket_activity']?'checked':''; ?>> + Log ticket's activity as internal notes. + </td> + </tr> + <tr><th>Staff Identity:</th> + <td> + <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked':''; ?>> + Hide staff's name on responses. + </td> + </tr> + <tr><th>Human Verification:</th> + <td> + <?php + if($config['enable_captcha'] && !$errors['enable_captcha']) { ?> + <img src="../captcha.php" border="0" align="left"> + <?php } ?> + <input type="checkbox" name="enable_captcha" <?php echo $config['enable_captcha']?'checked':''; ?>> + Enable captcha on new web tickets. <font class="error"> <?php echo $errors['enable_captcha']; ?></font><br/> + </td> + </tr> + + </table> + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2 >Email Settings</td></tr> + <tr class="subheader"><td colspan=2>Note that global settings can be disabled at dept/email level.</td></tr> + <tr><th valign="top"><br><b>Incoming Emails</b>:</th> + <td><i>For mail fetcher (POP/IMAP) to work you must set a cron job or simply enable auto-cron</i><br/> + <input type="checkbox" name="enable_mail_fetch" value=1 <?php echo $config['enable_mail_fetch']? 'checked': ''; ?> > Enable POP/IMAP email fetch + (<i>Global setting which can be disabled at email level</i>) <br/> + <input type="checkbox" name="enable_email_piping" value=1 <?php echo $config['enable_email_piping']? 'checked': ''; ?> > Enable email piping + (<i>You pipe we accept policy</i>)<br/> + <input type="checkbox" name="strip_quoted_reply" <?php echo $config['strip_quoted_reply'] ? 'checked':''; ?>> + Strip quoted reply (<i>depends on the tag below</i>)<br/> + <input type="text" name="reply_separator" value="<?php echo $config['reply_separator']; ?>"> Reply Separator Tag + <font class="error"> <?php echo $errors['reply_separator']; ?></font> + </td> + </tr> + <tr><th valign="top"><br><b>Outgoing Emails</b>:</th> + <td> + <i><b>Default Email:</b> Only applies to outgoing emails with no SMTP settings.</i><br/> + <select name="default_smtp_id" + onChange="document.getElementById('overwrite').style.display=(this.options[this.selectedIndex].value>0)?'block':'none';"> + <option value=0>Select One</option> + <option value=0 selected="selected">None: Use PHP mail function</option> + <?php + $emails=db_query('SELECT email_id,email,name,smtp_host FROM '.EMAIL_TABLE.' WHERE smtp_active=1'); + if($emails && db_num_rows($emails)) { + while (list($id,$email,$name,$host) = db_fetch_row($emails)){ + $email=$name?"$name <$email>":$email; + $email=sprintf('%s (%s)',$email,$host); + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_smtp_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + <?php + } + } ?> + </select> <font class="error"> <?php echo $errors['default_smtp_id']; ?></font><br/> + <span id="overwrite" style="display:<?php echo ($config['default_smtp_id']?'display':'none'); ?>"> + <input type="checkbox" name="spoof_default_smtp" <?php echo $config['spoof_default_smtp'] ? 'checked':''; ?>> + Allow spoofing (No Overwrite). <font class="error"> <?php echo $errors['spoof_default_smtp']; ?></font><br/> + </span> + </td> + </tr> + <tr><th>Default System Email:</th> + <td> + <select name="default_email_id"> + <option value=0 disabled>Select One</option> + <?php + $emails=db_query('SELECT email_id,email,name FROM '.EMAIL_TABLE); + while (list($id,$email,$name) = db_fetch_row($emails)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_email_id']==$id)?'selected':''; ?>><?php echo $email; ?></option> + <?php + } ?> + </select> + <font class="error">* <?php echo $errors['default_email_id']; ?></font></td> + </tr> + <tr><th valign="top">Default Alert Email:</th> + <td> + <select name="alert_email_id"> + <option value=0 disabled>Select One</option> + <option value=0 selected="selected">Use Default System Email (above)</option> + <?php + $emails=db_query('SELECT email_id,email,name FROM '.EMAIL_TABLE.' WHERE email_id != '.db_input($config['default_email_id'])); + while (list($id,$email,$name) = db_fetch_row($emails)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['alert_email_id']==$id)?'selected':''; ?>><?php echo $email; ?></option> + <?php + } ?> + </select> + <font class="error">* <?php echo $errors['alert_email_id']; ?></font> + <br/><i>Used to send out alerts and notices to staff.</i> + </td> + </tr> + <tr><th>System Admin Email Address:</th> + <td> + <input type="text" size=25 name="admin_email" value="<?php echo $config['admin_email']; ?>"> + <font class="error">* <?php echo $errors['admin_email']; ?></font></td> + </tr> + </table> + + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2>Autoresponders (Global Setting)</td></tr> + <tr class="subheader"><td colspan=2">This is global setting which can be disabled at department level.</td></tr> + <tr><th valign="top">New Ticket:</th> + <td><i>Autoresponse includes the ticket ID required to check status of the ticket</i><br> + <input type="radio" name="ticket_autoresponder" value="1" <?php echo $config['ticket_autoresponder']?'checked':''; ?> />Enable + <input type="radio" name="ticket_autoresponder" value="0" <?php echo !$config['ticket_autoresponder']?'checked':''; ?> />Disable + </td> + </tr> + <tr><th valign="top">New Ticket by Staff:</th> + <td><i>Notice sent when staff creates a ticket on behalf of the user (Staff can disable)</i><br> + <input type="radio" name="ticket_notice_active" value="1" <?php echo $config['ticket_notice_active']?'checked':''; ?> />Enable + <input type="radio" name="ticket_notice_active" value="0" <?php echo !$config['ticket_notice_active']?'checked':''; ?> />Disable + </td> + </tr> + <tr><th valign="top">New Message:</th> + <td><i>Message appended to an existing ticket confirmation</i><br> + <input type="radio" name="message_autoresponder" value="1" <?php echo $config['message_autoresponder']?'checked':''; ?> />Enable + <input type="radio" name="message_autoresponder" value="0" <?php echo !$config['message_autoresponder']?'checked':''; ?> />Disable + </td> + </tr> + <tr><th valign="top">Overlimit notice:</th> + <td><i>Ticket denied notice sent <b>only once</b> on limit violation to the user.</i><br/> + <input type="radio" name="overlimit_notice_active" value="1" <?php echo $config['overlimit_notice_active']?'checked':''; ?> />Enable + <input type="radio" name="overlimit_notice_active" value="0" <?php echo !$config['overlimit_notice_active']?'checked':''; ?> />Disable + <br><i><b>Note:</b> Admin gets alerts on ALL denials by default.</i><br> + </td> + </tr> + </table> + <table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2> Alerts & Notices</td></tr> + <tr class="subheader"><td colspan=2> + Notices sent to user use 'No Reply Email' whereas alerts to staff use 'Alert Email' set above as FROM address respectively.</td> + </tr> + <tr><th valign="top">New Ticket Alert:</th> + <td> + <input type="radio" name="ticket_alert_active" value="1" <?php echo $config['ticket_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="ticket_alert_active" value="0" <?php echo !$config['ticket_alert_active']?'checked':''; ?> />Disable + <br><i>Select recipients</i> <font class="error"> <?php echo $errors['ticket_alert_active']; ?></font><br> + <input type="checkbox" name="ticket_alert_admin" <?php echo $config['ticket_alert_admin']?'checked':''; ?>> Admin Email + <input type="checkbox" name="ticket_alert_dept_manager" <?php echo $config['ticket_alert_dept_manager']?'checked':''; ?>> Department Manager + <input type="checkbox" name="ticket_alert_dept_members" <?php echo $config['ticket_alert_dept_members']?'checked':''; ?>> Department Members (spammy) + </td> + </tr> + <tr><th valign="top">New Message Alert:</th> + <td> + <input type="radio" name="message_alert_active" value="1" <?php echo $config['message_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="message_alert_active" value="0" <?php echo !$config['message_alert_active']?'checked':''; ?> />Disable + <br><i>Select recipients</i> <font class="error"> <?php echo $errors['message_alert_active']; ?></font><br> + <input type="checkbox" name="message_alert_laststaff" <?php echo $config['message_alert_laststaff']?'checked':''; ?>> Last Respondent + <input type="checkbox" name="message_alert_assigned" <?php echo $config['message_alert_assigned']?'checked':''; ?>> Assigned Staff + <input type="checkbox" name="message_alert_dept_manager" <?php echo $config['message_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) + </td> + </tr> + <tr><th valign="top">New Internal Note Alert:</th> + <td> + <input type="radio" name="note_alert_active" value="1" <?php echo $config['note_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="note_alert_active" value="0" <?php echo !$config['note_alert_active']?'checked':''; ?> />Disable + <br><i>Select recipients</i> <font class="error"> <?php echo $errors['note_alert_active']; ?></font><br> + <input type="checkbox" name="note_alert_laststaff" <?php echo $config['note_alert_laststaff']?'checked':''; ?>> Last Respondent + <input type="checkbox" name="note_alert_assigned" <?php echo $config['note_alert_assigned']?'checked':''; ?>> Assigned Staff + <input type="checkbox" name="note_alert_dept_manager" <?php echo $config['note_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) + </td> + </tr> + <tr><th valign="top">Overdue Ticket Alert:</th> + <td> + <input type="radio" name="overdue_alert_active" value="1" <?php echo $config['overdue_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="overdue_alert_active" value="0" <?php echo !$config['overdue_alert_active']?'checked':''; ?> />Disable + <br><i>Admin Email gets an alert by default. Select additional recipients below</i> <font class="error"> <?php echo $errors['overdue_alert_active']; ?></font><br> + <input type="checkbox" name="overdue_alert_assigned" <?php echo $config['overdue_alert_assigned']?'checked':''; ?>> Assigned Staff + <input type="checkbox" name="overdue_alert_dept_manager" <?php echo $config['overdue_alert_dept_manager']?'checked':''; ?>> Department Manager + <input type="checkbox" name="overdue_alert_dept_members" <?php echo $config['overdue_alert_dept_members']?'checked':''; ?>> Department Members (spammy) + </td> + </tr> + <tr><th valign="top">System Errors:</th> + <td><i>Enabled errors are sent to admin email set above</i><br> + <input type="checkbox" name="send_sys_errors" <?php echo $config['send_sys_errors']?'checked':'checked'; ?> disabled>System Errors + <input type="checkbox" name="send_sql_errors" <?php echo $config['send_sql_errors']?'checked':''; ?>>SQL errors + <input type="checkbox" name="send_login_errors" <?php echo $config['send_login_errors']?'checked':''; ?>>Excessive Login attempts + </td> + </tr> + + </table> + </td></tr> + <tr> + <td style="padding:10px 0 10px 240px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> + </td> + </tr> + </form> +</table> diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..e2b883e1b465b7c939b0bc713b27f6ec03dcd325 --- /dev/null +++ b/include/staff/profile.inc.php @@ -0,0 +1,221 @@ +<?php +if(!defined('OSTSTAFFINC') || !$staff || !$thisstaff) die('Access Denied'); + +$info=$staff->getInfo(); +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +$info['id']=$staff->getId(); +?> +<form action="profile.php" method="post" id="save" autocomplete="off"> + <input type="hidden" name="do" value="update"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>My Account Profile</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Account Information</h4> + <em>Contact information.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Username: + </td> + <td><b><?php echo $staff->getUserName(); ?></b></td> + </tr> + + <tr> + <td width="180" class="required"> + First Name: + </td> + <td> + <input type="text" size="34" name="firstname" value="<?php echo $info['firstname']; ?>"> + <span class="error">* <?php echo $errors['firstname']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Last Name: + </td> + <td> + <input type="text" size="34" name="lastname" value="<?php echo $info['lastname']; ?>"> + <span class="error">* <?php echo $errors['lastname']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Email Address: + </td> + <td> + <input type="text" size="34" name="email" value="<?php echo $info['email']; ?>"> + <span class="error">* <?php echo $errors['email']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Phone Number: + </td> + <td> + <input type="text" size="22" name="phone" value="<?php echo $info['phone']; ?>"> + <span class="error"> <?php echo $errors['phone']; ?></span> + Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> + <span class="error"> <?php echo $errors['phone_ext']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Mobile Number: + </td> + <td> + <input type="text" size="22" name="mobile" value="<?php echo $info['mobile']; ?>"> + <span class="error"> <?php echo $errors['mobile']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Preferences</strong>: Profile preferences and settings.</em> + </th> + </tr> + <tr> + <td width="180" class="required"> + Time Zone: + </td> + <td> + <select name="timezone_id" id="timezone_id"> + <option value="0">— Select Time Zone —</option> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$offset, $tz)=db_fetch_row($res)){ + $sel=($info['timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['timezone_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Daylight Saving: + </td> + <td> + <input type="checkbox" name="daylight_saving" value="1" <?php echo $info['daylight_saving']?'checked="checked"':''; ?>> + Observe daylight saving + <em>(Current Time: <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['daylight_saving']); ?></strong>)</em> + </td> + </tr> + <tr> + <td width="180">Maximum Page size:</td> + <td> + <select name="max_page_size"> + <option value="0">— system default —</option> + <?php + $pagelimit=$info['max_page_size']?$info['max_page_size']:$cfg->getPageSize(); + for ($i = 5; $i <= 50; $i += 5) { + $sel=($pagelimit==$i)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>show %s records</option>',$i,$sel,$i); + } ?> + </select> per page. + </td> + </tr> + <tr> + <td width="180">Auto Refresh Rate:</td> + <td> + <select name="auto_refresh_rate"> + <option value="0">— disable —</option> + <?php + $y=1; + for($i=1; $i <=30; $i+=$y) { + $sel=($info['auto_refresh_rate']==$i)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>Every %s %s</option>',$i,$sel,$i,($i>1?'mins':'min')); + if($i>9) + $y=2; + } ?> + </select> + <em>(Tickets page refresh rate in minutes.)</em> + </td> + </tr> + <tr> + <td width="180">Default Signature:</td> + <td> + <select name="default_signature_type"> + <option value="none" selected="selected">— None —</option> + <?php + $options=array('mine'=>'My Signature','dept'=>'Dept. Signature (if set)'); + foreach($options as $k=>$v) { + echo sprintf('<option value="%s" %s>%s</option>', + $k,($info['default_signature_type']==$k)?'selected="selected"':'',$v); + } + ?> + </select> + <em>(You can change selection on ticket page)</em> + <span class="error"> <?php echo $errors['default_signature_type']; ?></span> + </td> + </tr> + <?php + //Show an option to show assigned tickets to admins & managers. + if($staff->isAdmin() || $staff->isManager()){ ?> + <tr> + <td>Show Assigned Tickets:</td> + <td> + <input type="checkbox" name="show_assigned_tickets" <?php echo $info['show_assigned_tickets']?'checked="checked"':''; ?>> + <em>Show assigned tickets on open queue.</em> + </td> + </tr> + <?php } ?> + <tr> + <th colspan="2"> + <em><strong>Password</strong>: To reset your password, provide your current password and a new password below. <span class="error"> <?php echo $errors['passwd']; ?></span></em> + </th> + </tr> + <tr> + <td width="180"> + Current Password: + </td> + <td> + <input type="password" size="18" name="cpasswd" value="<?php echo $info['cpasswd']; ?>"> + <span class="error"> <?php echo $errors['cpasswd']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + New Password: + </td> + <td> + <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>"> + <span class="error"> <?php echo $errors['passwd1']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Confirm New Password: + </td> + <td> + <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>"> + <span class="error"> <?php echo $errors['passwd2']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Signature</strong>: Optional signature used on outgoing emails. + <span class="error"> <?php echo $errors['signature']; ?></span></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="signature" cols="21" rows="5" style="width: 60%;"><?php echo $info['signature']; ?></textarea> + <br><em>Signature is made available as a choice, on ticket reply.</em> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:200px;"> + <input type="submit" name="submit" value="Save Changes"> + <input type="reset" name="reset" value="Reset Changes"> + <input type="button" name="cancel" value="Cancel Changes" onclick='window.location.href="index.php"'> +</p> +</form> diff --git a/include/staff/settings-alerts.inc.php b/include/staff/settings-alerts.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..a71b9e8a88d8da1ff155de7f73bff66ef6089aa8 --- /dev/null +++ b/include/staff/settings-alerts.inc.php @@ -0,0 +1,102 @@ +<form action="settings.php?t=alerts" method="post" id="save"> +<input type="hidden" name="t" value="alerts" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Alerts and Notices Setting</h4> + <em>Alerts sent to staff on ticket "events". Staff assignment takes precedence over team assignment.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="160">New Ticket Alert:</td> + <td> + <input type="radio" name="ticket_alert_active" value="1" <?php echo $config['ticket_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="ticket_alert_active" value="0" <?php echo !$config['ticket_alert_active']?'checked':''; ?> />Disable + <em>Alert sent out on new tickets <font class="error"> <?php echo $errors['ticket_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="ticket_alert_admin" <?php echo $config['ticket_alert_admin']?'checked':''; ?>> Admin Email + <input type="checkbox" name="ticket_alert_dept_manager" <?php echo $config['ticket_alert_dept_manager']?'checked':''; ?>> Department Manager + <input type="checkbox" name="ticket_alert_dept_members" <?php echo $config['ticket_alert_dept_members']?'checked':''; ?>> Department Members (spammy) + </td> + </tr> + <tr> + <td width="160">New Message Alert:</td> + <td> + <input type="radio" name="message_alert_active" value="1" <?php echo $config['message_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="message_alert_active" value="0" <?php echo !$config['message_alert_active']?'checked':''; ?> />Disable + <em>Alert sent out when a new message is appended to an existing ticket <font class="error"> <?php echo $errors['message_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="message_alert_laststaff" <?php echo $config['message_alert_laststaff']?'checked':''; ?>> Last Respondent + <input type="checkbox" name="message_alert_assigned" <?php echo $config['message_alert_assigned']?'checked':''; ?>> Assigned Staff + <input type="checkbox" name="message_alert_dept_manager" <?php echo $config['message_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) + </td> + </tr> + <tr> + <td width="160">New Internal Note Alert:</td> + <td> + <input type="radio" name="note_alert_active" value="1" <?php echo $config['note_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="note_alert_active" value="0" <?php echo !$config['note_alert_active']?'checked':''; ?> />Disable + <em>Alert sent out when a new internal note is posted <font class="error"> <?php echo $errors['note_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="note_alert_laststaff" <?php echo $config['note_alert_laststaff']?'checked':''; ?>> Last Respondent + <input type="checkbox" name="note_alert_assigned" <?php echo $config['note_alert_assigned']?'checked':''; ?>> Assigned Staff + <input type="checkbox" name="note_alert_dept_manager" <?php echo $config['note_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) + </td> + </tr> + <tr> + <td width="160">Ticket Assignment Alert:</td> + <td> + <input name="assigned_alert_active" value="1" checked="checked" type="radio">Enable + <input name="assigned_alert_active" value="0" type="radio">Disable + <em>Alert sent out to staff on ticket assignment <font class="error"> <?php echo $errors['assigned_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="assigned_alert_staff" <?php echo $config['assigned_alert_staff']?'checked':''; ?>> Assigned Staff + <input type="checkbox"name="assigned_alert_team_lead" <?php echo $config['assigned_alert_team_lead']?'checked':''; ?>>Team Lead (Team assignment) + <input type="checkbox"name="assigned_alert_team_members" <?php echo $config['assigned_alert_team_members']?'checked':''; ?>> + Team Members (spammy) + </td> + </tr> + <tr> + <td width="160">Ticket Transfer Alert:</td> + <td> + <input type="radio" name="transfer_alert_active" value="1" <?php echo $config['transfer_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="transfer_alert_active" value="0" <?php echo !$config['transfer_alert_active']?'checked':''; ?> />Disable + <em>Alert sent out to staff on ticket transfer <font class="error"> +<?php echo $errors['alert_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="transfer_alert_assigned" <?php echo $config['transfer_alert_assigned']?'checked':''; ?>> Assigned Staff/Team + <input type="checkbox" name="transfer_alert_dept_manager" <?php echo $config['transfer_alert_dept_manager']?'checked':''; ?>> Department Manager + <input type="checkbox" name="transfer_alert_dept_members" <?php echo $config['transfer_alert_dept_members']?'checked':''; ?>> Department Members + (spammy) + </td> + </tr> + <tr> + <td width="160">Overdue Ticket Alert:</td> + <td> + <input type="radio" name="overdue_alert_active" value="1" <?php echo $config['overdue_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="overdue_alert_active" value="0" <?php echo !$config['overdue_alert_active']?'checked':''; ?> />Disable + <em>Alert sent out when a ticket becomes overdue - admin email gets an alert by default. <font class="error"> <?php echo $errors['overdue_alert_active']; ?></font></em><br> + <strong>Recipients</strong>: + <input type="checkbox" name="overdue_alert_assigned" <?php echo $config['overdue_alert_assigned']?'checked':''; ?>> Assigned Staff/Team + <input type="checkbox" name="overdue_alert_dept_manager" <?php echo $config['overdue_alert_dept_manager']?'checked':''; ?>> Department Manager + <input type="checkbox" name="overdue_alert_dept_members" <?php echo $config['overdue_alert_dept_members']?'checked':''; ?>> Department Members (spammy) + </td> + </tr> + <tr> + <td width="160">System Alerts:</td> + <td><em><b>Enabled</b>: Errors are sent to system admin email (<?php echo $cfg->getAdminEmail(); ?>)</em><br> + <input type="checkbox" name="send_sys_errors" checked="checked" disabled="disabled">System Errors + <input type="checkbox" name="send_sql_errors" <?php echo $config['send_sql_errors']?'checked':''; ?>>SQL errors + <input type="checkbox" name="send_login_errors" <?php echo $config['send_login_errors']?'checked':''; ?>>Excessive Login attempts + </td> + </tr> + </tbody> +</table> +<p style="padding-left:200px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-attachments.inc.php b/include/staff/settings-attachments.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..572c5da5a52db9fcfda21a49069efb21d7c5deb0 --- /dev/null +++ b/include/staff/settings-attachments.inc.php @@ -0,0 +1,107 @@ +<?php +if(!($maxfileuploads=ini_get('max_file_uploads'))) + $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; + +?> +<form action="settings.php?t=attachments" method="post" id="save"> +<input type="hidden" name="t" value="attachments" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Attachments Settings</h4> + <em> Before enabling attachments make sure you understand PHP file upload settings and security issues related to file upload.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180">Allow Attachments:</td> + <td> + <input type="checkbox" name="allow_attachments" <?php echo $config['allow_attachments']?'checked="checked"':''; ?>><b>Allow Attachments</b> + <em>(Global Setting)</em> + <font class="error"> <?php echo $errors['allow_attachments']; ?></font> + </td> + </tr> + <tr> + <td width="180">Emailed Attachments:</td> + <td> + <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed files + <font class="error"> <?php echo $errors['allow_email_attachments']; ?></font> + </td> + </tr> + <tr> + <td width="180">Online Attachments:</td> + <td> + <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> > + Allow web upload + <input type="checkbox" name="allow_online_attachments_onlogin" <?php echo $config['allow_online_attachments_onlogin'] ?'checked="checked"':''; ?> > + Limit to authenticated users only. <em>(User must be logged in to upload files)</em> + <font class="error"> <?php echo $errors['allow_online_attachments']; ?></font> + </td> + </tr> + <tr> + <td>Max. User File Uploads:</td> + <td> + <select name="max_user_file_uploads"> + <?php + for($i = 1; $i <=$maxfileuploads; $i++) { + ?> + <option <?php echo $config['max_user_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> + <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> + <?php + } ?> + </select> + <em>(Number of files the user is allowed to upload simultaneously)</em> + <font class="error"> <?php echo $errors['max_user_file_uploads']; ?></font> + </td> + </tr> + <tr> + <td>Max. Staff File Uploads:</td> + <td> + <select name="max_staff_file_uploads"> + <?php + for($i = 1; $i <=$maxfileuploads; $i++) { + ?> + <option <?php echo $config['max_staff_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> + <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> + <?php + } ?> + </select> + <em>(Number of files the staff is allowed to upload simultaneously)</em> + <font class="error"> <?php echo $errors['max_staff_file_uploads']; ?></font> + </td> + </tr> + <tr> + <td width="180">Maximum File Size:</td> + <td> + <input type="text" name="max_file_size" value="<?php echo $config['max_file_size']; ?>"> in bytes. + <em>(Max <?php echo Format::file_size(ini_get('upload_max_filesize')); ?>)</em> + <font class="error"> <?php echo $errors['max_file_size']; ?></font> + </td> + </tr> + <tr> + <td width="180">Ticket Response Files:</td> + <td> + <input type="checkbox" name="email_attachments" <?php echo $config['email_attachments']?'checked="checked"':''; ?> >Email attachments to the user + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Accepted File Types</strong>: Limit the type of files users are allowed to upload. + <font class="error"> <?php echo $errors['allowed_filetypes']; ?></font></em> + </th> + </tr> + <tr> + <td colspan="2"> + <em>Enter allowed file extensions separated by a comma. e.g .doc, .pdf. To accept all files enter wildcard <b><i>.*</i></b> i.e dotStar (NOT Recommended).</em><br> + <textarea name="allowed_filetypes" cols="21" rows="4" style="width: 65%;" wrap="hard" ><?php echo $config['allowed_filetypes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:210px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-autoresponders.inc.php b/include/staff/settings-autoresponders.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..70bddc826e47c4000b8afca8c35998d36384486d --- /dev/null +++ b/include/staff/settings-autoresponders.inc.php @@ -0,0 +1,56 @@ +<form action="settings.php?t=autoresponders" method="post" id="save"> +<input type="hidden" name="t" value="autoresponders" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Autoresponder Setting</h4> + <em>Global setting - can be disabled at department or email level.</em> + </th> + </tr> + </thead> + <tbody> + + <tr> + <td width="160">New Ticket:</td> + <td> + <input type="radio" name="ticket_autoresponder" value="1" <?php echo $config['ticket_autoresponder']?'checked="checked"':''; ?> /><b>Enable</b> + <input type="radio" name="ticket_autoresponder" value="0" <?php echo !$config['ticket_autoresponder']?'checked="checked"':''; ?> />Disable + + <em>(Autoresponse includes the ticket ID required to check status of the ticket)</em> + </td> + </tr> + <tr> + <td width="160">New Ticket by staff:</td> + <td> + <input type="radio" name="ticket_notice_active" value="1" <?php echo $config['ticket_notice_active']?'checked="checked"':''; ?> /><b>Enable</b> + <input type="radio" name="ticket_notice_active" value="0" <?php echo !$config['ticket_notice_active']?'checked="checked"':''; ?> />Disable + + <em>(Notice sent when staff creates a ticket on behalf of the user (Staff can overwrite))</em> + </td> + </tr> + <tr> + <td width="160">New Message:</td> + <td> + <input type="radio" name="message_autoresponder" value="1" <?php echo $config['message_autoresponder']?'checked="checked"':''; ?> /><b>Enable</b> + <input type="radio" name="message_autoresponder" value="0" <?php echo !$config['message_autoresponder']?'checked="checked"':''; ?> />Disable + + <em>(Confirmation notice sent when a new message is appended to an existing ticket)</em> + </td> + </tr> + <tr> + <td width="160">Overlimit notice:</td> + <td> + <input type="radio" name="overlimit_notice_active" value="1" <?php echo $config['overlimit_notice_active']?'checked="checked"':''; ?> /><b>Enable</b> + <input type="radio" name="overlimit_notice_active" value="0" <?php echo !$config['overlimit_notice_active']?'checked="checked"':''; ?> />Disable + + <em>(Ticket denied notice sent to user on limit violation. Admin gets alerts on ALL denials by default)</em> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:200px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-dates.inc.php b/include/staff/settings-dates.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0434e94c73313ee76c790ddb90315d16636b0afb --- /dev/null +++ b/include/staff/settings-dates.inc.php @@ -0,0 +1,69 @@ +<?php +$gmtime=Misc::gmtime(); +?> +<form action="settings.php?t=dates" method="post" id="save"> +<input type="hidden" name="t" value="dates" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Date and Time Options</h4> + <em>Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em> + </th> + </tr> + </thead> + <tbody> + <tr><td width="220" class="required">Time Format:</td> + <td> + <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>"> + <font class="error">* <?php echo $errors['time_format']; ?></font> + <em><?php echo Format::date($config['time_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em></td> + </tr> + <tr><td width="220" class="required">Date Format:</td> + <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>"> + <font class="error">* <?php echo $errors['date_format']; ?></font> + <em><?php echo Format::date($config['date_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Date & Time Format:</td> + <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>"> + <font class="error">* <?php echo $errors['datetime_format']; ?></font> + <em><?php echo Format::date($config['datetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Day, Date & Time Format:</td> + <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>"> + <font class="error">* <?php echo $errors['daydatetime_format']; ?></font> + <em><?php echo Format::date($config['daydatetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Default Time Zone:</td> + <td> + <select name="default_timezone_id"> + <option value="">— Select Default Time Zone —</option> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$offset, $tz)=db_fetch_row($res)){ + $sel=($config['default_timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); + } + } + ?> + </select> + <font class="error">* <?php echo $errors['default_timezone_id']; ?></font> + </td> + </tr> + <tr> + <td width="220">Daylight Saving:</td> + <td> + <input type="checkbox" name="enable_daylight_saving" <?php echo $config['enable_daylight_saving'] ? 'checked="checked"': ''; ?>>Observe daylight savings + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..017eb29ec14b00dbc2575d812d5f3a2aa5727f3e --- /dev/null +++ b/include/staff/settings-emails.inc.php @@ -0,0 +1,109 @@ +<form action="settings.php?t=emails" method="post" id="save"> +<input type="hidden" name="t" value="emails" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Email Settings</h4> + <em>Note that some of the global settings can be overwritten at department/email level.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Default System Email:</td> + <td> + <select name="default_email_id"> + <option value=0 disabled>Select One</option> + <?php + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE; + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$email,$name) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + <?php + } + } ?> + </select> + <font class="error">* <?php echo $errors['default_email_id']; ?></font> + </td> + </tr> + <tr> + <td width="180" class="required">Default Alert Email:</td> + <td> + <select name="alert_email_id"> + <option value="0" selected="selected">Use Default System Email (above)</option> + <?php + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' WHERE email_id != '.db_input($config['default_email_id']); + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$email,$name) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['alert_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + <?php + } + } ?> + </select> + <font class="error">* <?php echo $errors['alert_email_id']; ?></font> + </td> + </tr> + <tr> + <td width="180" class="required">Admin Email Address:</td> + <td> + <input type="text" size=40 name="admin_email" value="<?php echo $config['admin_email']; ?>"> + <font class="error">* <?php echo $errors['admin_email']; ?></font></td> + </tr> + <tr><th colspan=2><em><strong>Incoming Emails</strong>: For mail fetcher (POP/IMAP) to work you must set a cron job or enable auto-cron</em></th> + <tr> + <td width="180">Email Polling:</td> + <td><input type="checkbox" name="enable_mail_polling" value=1 <?php echo $config['enable_mail_polling']? 'checked="checked"': ''; ?> > Enable POP/IMAP + <em>(Global setting which can be disabled at email level)</em> + </td> + </tr> + <tr> + <td width="180">Email Piping:</td> + <td><input type="checkbox" name="enable_email_piping" value=1 <?php echo $config['enable_email_piping']? 'checked="checked"': ''; ?>> Enable email piping + <em>(You pipe we accept policy)</em> + </td> + </tr> + <tr> + <td width="180">Strip Quoted Reply:</td> + <td> + <input type="checkbox" name="strip_quoted_reply" <?php echo $config['strip_quoted_reply'] ? 'checked="checked"':''; ?>> + <em>(depends on the reply separator tag set below)</em> + <font class="error"> <?php echo $errors['strip_quoted_reply']; ?></font> + </td> + </tr> + <tr> + <td width="180">Reply Separator Tag:</td> + <td><input type="text" name="reply_separator" value="<?php echo $config['reply_separator']; ?>"> + <font class="error"> <?php echo $errors['reply_separator']; ?></font> + </td> + </tr> + <tr><th colspan=2><em><strong>Outgoing Emails</strong>: Default email only applies to outgoing emails without SMTP setting.</em></th></tr> + <tr><td width="180">Default Outgoing Email:</td> + <td> + <select name="default_smtp_id"> + <option value=0 selected="selected">None: Use PHP mail function</option> + <?php + $sql='SELECT email_id,email,name,smtp_host FROM '.EMAIL_TABLE.' WHERE smtp_active=1'; + + if(($res=db_query($sql)) && db_num_rows($res)) { + while (list($id,$email,$name,$host) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_smtp_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + <?php + } + } ?> + </select> <font class="error"> <?php echo $errors['default_smtp_id']; ?></font> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-general.inc.php b/include/staff/settings-general.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..ff5e058b1e36bdb2a7b51282201c9b5c3e633412 --- /dev/null +++ b/include/staff/settings-general.inc.php @@ -0,0 +1,196 @@ +<form action="settings.php?t=general" method="post" id="save"> +<input type="hidden" name="t" value="general" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>General Settings</h4> + <em>Offline mode will disable client interface and only allow admins to login to Staff Control Panel</em> + </th> + </tr> + </thead> + <tbody> + + <tr> + <td width="220" class="required">Helpdesk Status:</td> + <td> + <input type="radio" name="isonline" value="1" <?php echo $config['isonline']?'checked="checked"':''; ?> /><b>Online</b> (Active) + <input type="radio" name="isonline" value="0" <?php echo !$config['isonline']?'checked="checked"':''; ?> /><b>Offline</b> (Disabled) + <font class="error"> <?php echo $config['isoffline']?'osTicket offline':''; ?></font> + </td> + </tr> + <tr> + <td width="220" class="required">Helpdesk URL:</td> + <td> + <input type="text" size="40" name="helpdesk_url" value="<?php echo $config['helpdesk_url']; ?>"> + <font class="error">* <?php echo $errors['helpdesk_url']; ?></font></td> + </tr> + <tr> + <td width="220" class="required">Helpdesk Name/Title:</td> + <td><input type="text" size="40" name="helpdesk_title" value="<?php echo $config['helpdesk_title']; ?>"> + <font class="error">* <?php echo $errors['helpdesk_title']; ?></font></td> + </tr> + <tr> + <td width="220" class="required">Default Department:</td> + <td> + <select name="default_dept_id"> + <option value="">— Select Default Department —</option> + <?php + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' WHERE ispublic=1'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$name) = db_fetch_row($res)){ + $selected = ($config['default_dept_id']==$id)?'selected="selected"':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?> Dept</option> + <?php + } + } ?> + </select> <font class="error">* <?php echo $errors['default_dept_id']; ?></font> + </td> + </tr> + <tr> + <td width="220" class="required">Default Email Templates:</td> + <td> + <select name="default_template_id"> + <option value="">— Select Default Template —</option> + <?php + $sql='SELECT tpl_id,name FROM '.EMAIL_TEMPLATE_TABLE.' WHERE isactive=1 AND cfg_id='.db_input($cfg->getId()).' ORDER BY name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$name) = db_fetch_row($res)){ + $selected = ($config['default_template_id']==$id)?'selected="selected"':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + <?php + } + } ?> + </select> <font class="error">* <?php echo $errors['default_template_id']; ?></font> + </td> + </tr> + + <tr><td>Default Page Size:</td> + <td> + <select name="max_page_size"> + <?php + $pagelimit=$config['max_page_size']; + for ($i = 5; $i <= 50; $i += 5) { + ?> + <option <?php echo $config['max_page_size']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"><?php echo $i; ?></option> + <?php + } ?> + </select> + </td> + </tr> + <tr> + <td>Default Log Level:</td> + <td> + <select name="log_level"> + <option value=0 <?php echo $config['log_level'] == 0 ? 'selected="selected"':''; ?>>None (Disable Logger)</option> + <option value=3 <?php echo $config['log_level'] == 3 ? 'selected="selected"':''; ?>> DEBUG</option> + <option value=2 <?php echo $config['log_level'] == 2 ? 'selected="selected"':''; ?>> WARN</option> + <option value=1 <?php echo $config['log_level'] == 1 ? 'selected="selected"':''; ?>> ERROR</option> + </select> + <font class="error"> <?php echo $errors['log_level']; ?></font> + </td> + </tr> + <tr> + <td>Purge Logs:</td> + <td> + <select name="log_graceperiod"> + <option value=0 selected>Never Purge Logs</option> + <?php + for ($i = 1; $i <=12; $i++) { + ?> + <option <?php echo $config['log_graceperiod']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> + After <?php echo $i; ?> <?php echo ($i>1)?'Months':'Month'; ?></option> + <?php + } ?> + </select> + </td> + </tr> + <tr><td>Password Reset Policy:</th> + <td> + <select name="passwd_reset_period"> + <option value="0"> — None —</option> + <?php + for ($i = 1; $i <= 12; $i++) { + echo sprintf('<option value="%d" %s>%s%s</option>', + $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''),$i>1?"Every $i ":'',$i>1?' Months':'Monthly'); + } + ?> + </select> + <font class="error"> <?php echo $errors['passwd_reset_period']; ?></font> + </td> + </tr> + <tr><td>Staff Excessive Logins:</td> + <td> + <select name="staff_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_max_logins']==$i)?'selected="selected"':''),$i); + } + ?> + </select> failed login attempt(s) allowed before a + <select name="staff_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_login_timeout']==$i)?'selected="selected"':''),$i); + } + ?> + </select> minute lock-out is enforced. + </td> + </tr> + <tr><td>Staff Session Timeout:</td> + <td> + <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>"> + Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). + </td> + </tr> + <tr><td>Bind Staff Session to IP:</td> + <td> + <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> + <em>(binds staff session to originating IP address upon login)</em> + </td> + </tr> + <tr><td>Client Excessive Logins:</td> + <td> + <select name="client_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_max_logins']==$i)?'selected="selected"':''),$i); + } + + ?> + </select> failed login attempt(s) allowed before a + <select name="client_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_login_timeout']==$i)?'selected="selected"':''),$i); + } + ?> + </select> minute lock-out is enforced. + </td> + </tr> + + <tr><td>Client Session Timeout:</td> + <td> + <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>"> + Maximum idle time in minutes before a client must log in again (enter 0 to disable). + </td> + </tr> + <tr><td>Clickable URLs:</td> + <td> + <input type="checkbox" name="clickable_urls" <?php echo $config['clickable_urls']?'checked="checked"':''; ?>> + <em>(converts URLs in messages to clickable links)</em> + </td> + </tr> + <tr><td>Enable Auto Cron:</td> + <td> + <input type="checkbox" name="enable_auto_cron" <?php echo $config['enable_auto_cron']?'checked="checked"':''; ?>> + <em>(executes cron jobs based on staff activity - not recommended)</em> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..2d368b3bac7702f9a8ff9e76f1c1edd645a1799e --- /dev/null +++ b/include/staff/settings-kb.inc.php @@ -0,0 +1,37 @@ +<?php +?> +<form action="settings.php?t=kb" method="post" id="save"> +<input type="hidden" name="t" value="kb" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Knowledgebase Settings</h4> + <em>Disabling knowledgebase disables user's knowledgebase interface.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180">Knowledgebase Status:</td> + <td> + <input type="checkbox" name="enable_kb" value="1" <?php echo $config['enable_kb']?'checked="checked"':''; ?>> + Enable Knowledgebase <em>(Client Interface)</em> + <font class="error"> <?php echo $errors['enable_kb']; ?></font> + </td> + </tr> + <tr> + <td width="180">Premade Responses:</td> + <td> + <input type="checkbox" name="enable_premade" value="1" <?php echo $config['enable_premade']?'checked="checked"':''; ?> > + Enable premade/canned responses <em>(Available on ticket reply)</em> + <font class="error"> <?php echo $errors['enable_premade']; ?></font> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:210px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..bcf7347a74cf6d5bf5e9c60cfbe1b28729bbaffd --- /dev/null +++ b/include/staff/settings-tickets.inc.php @@ -0,0 +1,140 @@ +<form action="settings.php?t=tickets" method="post" id="save"> +<input type="hidden" name="t" value="tickets" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Ticket Settings and Options</h4> + <em>Global ticket settings and options.</em> + </th> + </tr> + </thead> + <tbody> + <tr><td width="220" class="required">Ticket IDs:</td> + <td> + <input type="radio" name="random_ticket_ids" value="0" <?php echo !$config['random_ticket_ids']?'checked="checked"':''; ?> /> + Sequential + <input type="radio" name="random_ticket_ids" value="1" <?php echo $config['random_ticket_ids']?'checked="checked"':''; ?> /> + Random <em>(highly recommended)</em> + </td> + </tr> + + <tr> + <td width="180" class="required"> + Default SLA: + </td> + <td> + <select name="default_sla_id"> + <option value="0">— None —</option> + <?php + $sql='SELECT id,name FROM '.SLA_TABLE.' sla ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($config['default_sla_id'] && $id==$config['default_sla_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['default_sla_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required">Default Priority:</td> + <td> + <select name="default_priority_id"> + <?php + $priorities= db_query('SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE); + while (list($id,$tag) = db_fetch_row($priorities)){ ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_priority_id']==$id)?'selected':''; ?>><?php echo $tag; ?></option> + <?php + } ?> + </select> + <span class="error">* <?php echo $errors['default_priority_id']; ?></span> + </td> + </tr> + <tr> + <td width="180">Web Tickets Priority</td> + <td> + <input type="checkbox" name="allow_priority_change" value="1" <?php echo $config['allow_priority_change'] ?'checked="checked"':''; ?>> + <em>(Allow user to overwrite/set priority)</em> + </td> + </tr> + <tr> + <td width="180">Emailed Tickets Priority</td> + <td> + <input type="checkbox" name="use_email_priority" value="1" <?php echo $config['use_email_priority'] ?'checked="checked"':''; ?> > + <em>(Use email priority when available)</em> + </td> + </tr> + <tr> + <td width="180">Show Related Tickets</td> + <td> + <input type="checkbox" name="show_related_tickets" value="1" <?php echo $config['show_related_tickets'] ?'checked="checked"':''; ?> > + <em>(Show all related tickets on user login - otherwise access is restricted to one ticket view per login)</em> + </td> + </tr> + <tr> + <td>Human Verification:</td> + <td> + <input type="checkbox" name="enable_captcha" <?php echo $config['enable_captcha']?'checked="checked"':''; ?>> + Enable CAPTCHA on new web tickets.<em>(requires GDLib)</em> <font class="error"> <?php echo $errors['enable_captcha']; ?></font><br/> + </td> + </tr> + <tr> + <td>Maximum <b>Open</b> Tickets:</td> + <td> + <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>"> + per email/user. <em>(Helps with spam and email flood control - enter 0 for unlimited)</em> + </td> + </tr> + <tr> + <td>Ticket Auto-lock Time:</td> + <td> + <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>"> + <font class="error"><?php echo $errors['autolock_minutes']; ?></font> + <em>(Minutes to lock a ticket on activity - enter 0 to disable locking)</em> + </td> + </tr> + <tr> + <td>Reopened Tickets:</td> + <td> + <input type="checkbox" name="auto_assign_reopened_tickets" <?php echo $config['auto_assign_reopened_tickets']?'checked="checked"':''; ?>> + Auto-assign reopened tickets to the last available respondent. + </td> + </tr> + <tr> + <td>Assigned Tickets:</td> + <td> + <input type="checkbox" name="show_assigned_tickets" <?php echo $config['show_assigned_tickets']?'checked="checked"':''; ?>> + Show assigned tickets on open queue. + </td> + </tr> + <tr> + <td>Answered Tickets:</td> + <td> + <input type="checkbox" name="show_answered_tickets" <?php echo $config['show_answered_tickets']?'checked="checked"':''; ?>> + Show answered tickets on open queue. + </td> + </tr> + <tr> + <td>Ticket Activity Log:</td> + <td> + <input type="checkbox" name="log_ticket_activity" <?php echo $config['log_ticket_activity']?'checked="checked"':''; ?>> + Log ticket activity as internal notes. + </td> + </tr> + <tr> + <td>Staff Identity Masking:</td> + <td> + <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked="checked"':''; ?>> + Hide staff's name on responses. + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings.php b/include/staff/settings.php new file mode 100644 index 0000000000000000000000000000000000000000..1dac69a5c1e8ce899e2c6b79ce5d85cb90c79d84 --- /dev/null +++ b/include/staff/settings.php @@ -0,0 +1,721 @@ +<?php include "./include/header.php" ?> +<h2>System Preferences and Settings (v1.6 ST)</h2> + +<form action="settings.php" method="post"> +<br> +<a href="#" class="expand_all">Expand All</a> | +<a href="#" class="collapse_all">Collapse All</a> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> General Settings</a></h4> + <em>Offline mode will disable client interface and only allow super admins to login to Staff Control Panel</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220" class="required"> + Helpdesk Status: + </td> + <td> + <input type="radio" name="isonline" value="1" checked="checked"><strong>Online</strong> (Active) + <input type="radio" name="isonline" value="0"><strong>Offline</strong> (Disabled) + <span class="warn"> </span> + </td> + </tr> + <tr> + <td width="220" class="required"> + Helpdesk URL: + </td> + <td> + <input type="text" size="40" name="helpdesk_url" value="http://helpdesk.enhancesoft.com/"> + <span class="error"> </span> + </td> + </tr> + <tr> + <td width="220"> + Helpdesk Name/Title: + </td> + <td> + <input type="text" size="40" name="helpdesk_title" value="Enhancesoft :: Support Ticket System"> + </td> + </tr> + <tr> + <td width="220" class="required"> + Default E-Mail Templates: + </td> + <td> + <select name="default_template_id"> + <option value="0">Select Default Template</option> + <option value="1">osTicket Default Template</option> + <option value="3" selected="selected">No Links</option> + </select> + <span class="error"> </span> + </td> + </tr> + <tr> + <td width="220" class="required"> + Default Department: + </td> + <td> + <select name="default_dept_id"> + <option value="0">Select Default Dept</option> + <option value="1" selected="selected">Support Dept</option> + <option value="2">Billing Dept</option> + <option value="4">Test Dept</option> + </select> + <span class="error"> </span> + </td> + </tr> + <tr> + <td width="220"> + Default Page Size: + </td> + <td> + <select name="max_page_size"> + <option value="5">5</option> + <option value="10">10</option> + <option value="15">15</option> + <option value="20">20</option> + <option value="25" selected="selected">25</option> + <option value="30">30</option> + <option value="35">35</option> + <option value="40">40</option> + <option value="45">45</option> + <option value="50">50</option> + </select> + </td> + </tr> + <tr> + <td width="220"> + Default Log Level: + </td> + <td> + <select name="log_level"> + <option value="0">None (Disable Logger)</option> + <option value="3">DEBUG</option> + <option value="2" selected="selected">WARN</option> + <option value="1">ERROR</option> + </select> + </td> + </tr> + <tr> + <td width="220"> + Purge Logs: + </td> + <td> + <select name="log_graceperiod"> + <option value="0" selected>Never Purge Logs</option> + <option value="1">After 1 Month</option> + <option value="2">After 2 Months</option> + <option value="3">After 3 Months</option> + <option value="4">After 4 Months</option> + <option value="5">After 5 Months</option> + <option value="6">After 6 Months</option> + <option value="7">After 7 Months</option> + <option value="8">After 8 Months</option> + <option value="9">After 9 Months</option> + <option value="10">After 10 Months</option> + <option value="11">After 11 Months</option> + <option value="12">After 12 Months</option> + </select> + </td> + </tr> + <tr> + <td width="220"> + Excessive Staff Logins: + </td> + <td> + <select name="staff_max_logins"> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + <option value="4" selected="selected">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + </select> failed login attempt(s) allowed before a + <select name="staff_login_timeout"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + </select> minute lock-out is enforced. + </td> + </tr> + <tr> + <td width="220"> + Staff Session Timeout: + </td> + <td> + <input type="text" name="staff_session_timeout" size="4" value="0"> + Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). + </td> + </tr> + <tr> + <td width="220"> + Staff Session IP Binding: + </td> + <td> + <input type="checkbox" name="staff_ip_binding" checked="checked" value="1"> + <em>(binds staff session to originating IP address upon login)</em> + </td> + </tr> + <tr> + <td width="220"> + Excessive Client Logins: + </td> + <td> + <select name="client_max_logins"> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + <option value="4" selected="selected">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + </select> failed login attempt(s) allowed before a + <select name="client_login_timeout"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + </select> minute lock-out is enforced. + </td> + </tr> + <tr> + <td width="220"> + Client Session Timeout: + </td> + <td> + <input type="text" name="client_session_timeout" size="4" value="0"> + Maximum idle time in minutes before a client must log in again (enter 0 to disable). + </td> + </tr> + <tr> + <td width="220"> + Clickable URLs: + </td> + <td> + <input type="checkbox" name="clickable_urls" checked="checked" value="1"> + <em>(converts URLs in messages to clickable links)</em> + </td> + </tr> + <tr> + <td width="220"> + Enable Auto-cron: + </td> + <td> + <input type="checkbox" name="enable_auto_cron" value="1"> + <em>(executes cron jobs based on staff activity - not recommended)</em> + </td> + </tr> + </tbody> +</table> + +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> Date and Time Settings</a></h4> + <em>Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220" class="required"> + Time Format: + </td> + <td> + <input type="text" name="time_format" value="h:i A"> + <span class="error"> </span> + <em> 09:24 AM</em> + </td> + </tr> + <tr> + <td width="220" class="required"> + Date Format: + </td> + <td> + <input type="text" name="date_format" value="m/d/Y"> + <span class="error"> </span> + <em>05/06/2011</em> + </td> + </tr> + <tr> + <td width="220" class="required"> + Date & Time Format: + </td> + <td> + <input type="text" name="datetime_format" value="m/d/Y g:i a"> + <span class="error"> </span> + <em>05/06/2011 9:24 am</em> + </td> + </tr> + <tr> + <td width="220" class="required"> + Day, Date & Time Format: + </td> + <td> + <input type="text" name="daydatetime_format" value="D, M j Y g:ia"> + <span class="error">* </span> + <em>Fri, May 6 2011 9:24am</em> + </td> + </tr> + <tr> + <td width="220"> + Default Timezone: + </td> + <td> + <select name="timezone_offset"> + <option value="0">Server Time (GMT 0:00)</option> <option value="-12.0">GMT -12.0 (Eniwetok, Kwajalein)</option> + <option value="-11.0">GMT -11.0 (Midway Island, Samoa)</option> + <option value="-10.0">GMT -10.0 (Hawaii)</option> + <option value="-9.0">GMT -9.0 (Alaska)</option> + <option value="-8.0">GMT -8.0 (Pacific Time (US & Canada))</option> + <option value="-7.0">GMT -7.0 (Mountain Time (US & Canada))</option> + <option value="-6.0">GMT -6.0 (Central Time (US & Canada), Mexico City)</option> + <option value="-5.0" selected="selected">GMT -5.0 (Eastern Time (US & Canada), Bogota, Lima)</option> + <option value="-4.0">GMT -4.0 (Atlantic Time (Canada), Caracas, La Paz)</option> + <option value="-3.5">GMT -3.5 (Newfoundland)</option> + <option value="-3.0">GMT -3.0 (Brazil, Buenos Aires, Georgetown)</option> + <option value="-2.0">GMT -2.0 (Mid-Atlantic)</option> + <option value="-1.0">GMT -1.0 (Azores, Cape Verde Islands)</option> + <option value="0.0">GMT 0.0 (Western Europe Time, London, Lisbon, Casablanca)</option> + <option value="1.0">GMT 1.0 (Brussels, Copenhagen, Madrid, Paris)</option> + <option value="2.0">GMT 2.0 (Kaliningrad, South Africa)</option> + <option value="3.0">GMT 3.0 (Baghdad, Riyadh, Moscow, St. Petersburg)</option> + <option value="3.5">GMT 3.5 (Tehran)</option> + <option value="4.0">GMT 4.0 (Abu Dhabi, Muscat, Baku, Tbilisi)</option> + <option value="4.5">GMT 4.5 (Kabul)</option> + <option value="5.0">GMT 5.0 (Ekaterinburg, Islamabad, Karachi, Tashkent)</option> + <option value="5.5">GMT 5.5 (Bombay, Calcutta, Madras, New Delhi)</option> + <option value="6.0">GMT 6.0 (Almaty, Dhaka, Colombo)</option> + <option value="7.0">GMT 7.0 (Bangkok, Hanoi, Jakarta)</option> + <option value="8.0">GMT 8.0 (Beijing, Perth, Singapore, Hong Kong)</option> + <option value="9.0">GMT 9.0 (Tokyo, Seoul, Osaka, Sapporo, Yakutsk)</option> + <option value="9.5">GMT 9.5 (Adelaide, Darwin)</option> + <option value="10.0">GMT 10.0 (Eastern Australia, Guam, Vladivostok)</option> + <option value="11.0">GMT 11.0 (Magadan, Solomon Islands, New Caledonia)</option> + <option value="12.0">GMT 12.0 (Auckland, Wellington, Fiji, Kamchatka)</option> + </select> + </td> + </tr> + <tr> + <td width="220"> + Daylight Savings + </td> + <td> + <input type="checkbox" name="daylight_savings" value="1"> + <em>observe daylight savings time</em> + </td> + </tr> + </tbody> +</table> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> Ticket Options and Settings</a></h4> + <em>If enabled ticket lock get auto-renewed on form activity.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220"> + Ticket IDs: + </td> + <td> + <input type="radio" name="random_ticket_ids" value="0"> Sequential + <input type="radio" name="random_ticket_ids" value="1" checked="checked">Random (recommended) + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + Ticket Priority: + </td> + <td> + <select name="default_priority_id"> + <option value="1">Low</option> + <option value="2" selected="selected">Normal</option> + <option value="3">High</option> + <option value="4">Emergency</option> + </select> Default Priority<br> + <input type="checkbox" name="allow_priority_change" > + Allow user to overwrite/set priority (new web tickets)<br> + + <input type="checkbox" name="use_email_priority" > + Use email priority when available (new emailed tickets) + </td> + </tr> + <tr> + <td width="220"> + Maximum <strong>Open</strong> Tickets: + </td> + <td> + <input type="text" name="max_open_tickets" size="4" value="0"> + per email <em>(helps with spam and flood control - enter 0 for unlimited)</em> + </td> + </tr> + <tr> + <td width="220"> + Ticket Auto-lock Time: + </td> + <td> + <input type="text" name="autolock_minutes" size="4" value="3"> + <em>(minutes to lock a ticket on activity - enter 0 to disable locking)</em> + </td> + </tr> + <tr> + <td width="220"> + Ticket Grace Period: + </td> + <td> + <input type="text" name="overdue_grace_period" size=4 value="0"> + <em>(hours before ticket is marked overdue - enter 0 to disable aging)</em> + </td> + </tr> + <tr> + <td width="220"> + Reopened Tickets: + </td> + <td> + <input type="checkbox" name="auto_assign_reopened_tickets" checked="checked"> + Auto-assign reopened tickets to last available respondent. <em>(3 months limit)</em> + </td> + </tr> + <tr> + <td width="220"> + Assigned Tickets: + </td> + <td> + <input type="checkbox" name="show_assigned_tickets"> + Show assigned tickets on open queue. + </td> + </tr> + <tr> + <td width="220"> + Answered Tickets: + </td> + <td> + <input type="checkbox" name="show_nswered_tickets"> + Show answered tickets on open queue. + </td> + </tr> + <tr> + <td width="220"> + Ticket Activity Log: + </td> + <td> + <input type="checkbox" name="log_ticket_activity"> + Log ticket activity as an internal note. + </td> + </tr> + <tr> + <td width="220"> + Staff Identity Masking: + </td> + <td> + <input type="checkbox" name="hide_staff_name"> + Hide staff's name on responses. + </td> + </tr> + <tr> + <td width="220"> + Human Verification: + </td> + <td> + <input type="checkbox" name="enable_captcha"> + Enable CAPTCHA on new web tickets. + <em>(requires GDLib)</em> + </td> + </tr> + </tbody> +</table> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> E-mail Settings</a></h4> + <em>Note that global settings can be disabled at dept/e-mail level.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220" class="required multi-line"> + Incoming Email: + <br><em>For mail fetcher (POP/IMAP) to work you must set a cron job or enable auto-cron</em> + </td> + <td> + <input type="checkbox" name="enable_mail_fetch" value="1" checked="checked"> Enable POP/IMAP email fetch + <em>(Global setting which can be disabled at email level)</em><br> + + <input type="checkbox" name="enable_email_piping" value="1" checked="checked"> Enable email piping + <em>(You pipe we accept policy)</em><br> + + <input type="checkbox" name="strip_quoted_reply" checked="checked"> + Strip quoted reply <em>(depends on the tag below)</em><br><br> + + Reply Separator Tag: + <input type="text" name="reply_separator" value="-- do not edit --"> + <span class="error"> </span> + </td> + </tr> + <tr> + <td width="220" class="required multi-line"> + Outgoing Email: + <br><em><strong>Default Email:</strong> Only applies to outgoing emails with no SMTP settings.</em><br/> + + </td> + <td> + <select name="default_smtp_id" onChange="document.getElementById('overwrite').style.display=(this.options[this.selectedIndex].value>0)?'block':'none';"> + <option value="0">Select One</option> + <option value="0">None: Use PHP mail function</option> + <option value="1" selected="selected">osTicket Support <support@osticket.com> (smtp.gmail.com)</option> + </select> + + <span id="overwrite" style="display:display"> + <br><input type="checkbox" name="spoof_default_smtp" > + Allow spoofing (No Overwrite). + </span> + </td> + </tr> + <tr> + <td width="220" class="required"> + Default System E-Mail: + </td> + <td> + <select name="default_email_id"> + <option value="0">Select One</option> + <option value="1" selected="selected">osTicket Support <support@osticket.com></option> + <option value="2">osTicket Alerts <alerts@osticket.com></option> + <option value="3">noreply@osticket.com</option> + <option value="5">lvcta.com (Test) <support@lvcta.com></option> + </select> + </td> + </tr> + <tr> + <td width="220" class="required"> + Default Alert E-Mail: + </td> + <td> + <select name="alert_email_id"> + <option value="0">Select One</option> + <option value="1">osTicket Support <support@osticket.com></option> + <option value="2" selected="selected">osTicket Alerts <alerts@osticket.com></option> + <option value="3">noreply@osticket.com</option> + <option value="5">lvcta.com (Test) <support@lvcta.com></option> + </select> + </td> + </tr> + <tr> + <td width="220" class="required"> + System Admin E-mail Address: + </td> + <td> + <input type="text" size="25" name="admin_email" value="peter@osticket.com"> + <span class="error"> </span> + </td> + </tr> + </tbody> +</table> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> Autoresponders (Global Setting)</a></h4> + <em>This is global setting which can be disabled at department level.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220" class="multi-line"> + New Ticket: + </td> + <td> + <em>Autoresponse includes the ticket ID required to check status of the ticket</em><br> + <input type="radio" name="ticket_autoresponder" value="1">Enable + <input type="radio" name="ticket_autoresponder" value="0" checked="checked">Disable + <br><br> + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + New Ticket by Staff: + </td> + <td> + <em>Notice sent when staff creates a ticket on behalf of the user (Staff can disable)</em><br> + <input type="radio" name="ticket_notice_active" value="1" checked="checked">Enable + <input type="radio" name="ticket_notice_active" value="0">Disable + <br><br> + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + New Message: + </td> + <td> + <em>Message appended to an existing ticket confirmation</em><br> + <input type="radio" name="message_autoresponder" value="1">Enable + <input type="radio" name="message_autoresponder" value="0" checked="checked">Disable + <br><br> + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + Ticket Denied: + </td> + <td> + <em>Ticket denied notice sent <strong>only once</strong> on limit violation to the user.</em><br> + <input type="radio" name="overlimit_notice_active" value="1">Enable + <input type="radio" name="overlimit_notice_active" value="0" checked="checked">Disable + <em><strong>Note:</strong> Admin gets alerts on ALL denials by default.</em> + <br><br> + </td> + </tr> + </tbody> +</table> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><a href="#"><span>–</span> Alerts and Notices</a></h4> + <em>Notices sent to user use 'No Reply Email' whereas alerts to staff use 'Alert Email' set above as FROM address respectively.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="220" class="multi-line"> + New Ticket Alert: + </td> + <td> + <input type="radio" name="ticket_alert_active" value="1" checked="checked">Enable + <input type="radio" name="ticket_alert_active" value="0">Disable + <br> + <strong>Select recipients:</strong> + <input type="checkbox" name="ticket_alert_admin" checked="checked"> Admin Email + <input type="checkbox" name="ticket_alert_dept_manager"> Department Manager + <input type="checkbox" name="ticket_alert_dept_members"> Department Members (spammy) + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + New Message Alert: + </td> + <td> + <input type="radio" name="message_alert_active" value="1" checked="checked">Enable + <input type="radio" name="message_alert_active" value="0">Disable + <br> + <strong>Select recipients:</strong> + <input type="checkbox" name="message_alert_laststaff" checked="checked"> Last Respondent + <input type="checkbox" name="message_alert_assigned" checked="checked"> Assigned Staff + <input type="checkbox" name="message_alert_dept_manager"> Department Manager (spammy) + </td> + </tr> + <tr> + <td width="220"> + New Internal Note Alert: + </td> + <td> + <input type="radio" name="note_alert_active" value="1" checked="checked">Enable + <input type="radio" name="note_alert_active" value="0">Disable + <br> + <strong>Select recipients:</strong> + <input type="checkbox" name="note_alert_laststaff" checked="checked"> Last Respondent + <input type="checkbox" name="note_alert_assigned" checked="checked"> Assigned Staff + <input type="checkbox" name="note_alert_dept_manager"> Department Manager (spammy) + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + Overdue Ticket Alert: + </td> + <td> + <input type="radio" name="overdue_alert_active" value="1" checked="checked">Enable + <input type="radio" name="overdue_alert_active" value="0">Disable + <br> + <strong>Select recipients:</strong> + <input type="checkbox" name="overdue_alert_assigned" checked="checked"> Assigned Staff + <input type="checkbox" name="overdue_alert_dept_manager" checked="checked"> Department Manager + <input type="checkbox" name="overdue_alert_dept_members"> Department Members (spammy) + <br><em><strong>Note:</strong> Admin gets all overdue alerts by default.</em> + </td> + </tr> + <tr> + <td width="220" class="multi-line"> + System Errors: + </td> + <td> + <input type="checkbox" name="send_sys_errors" checked="checked" disabled="disabled">System Errors + <input type="checkbox" name="send_sql_errors" checked="checked">SQL errors + <input type="checkbox" name="send_login_errors" checked="checked">Excessive Login attempts + <br><em>Enabled errors are sent to admin email set above</em> + </td> + </tr> + </tbody> +</table> +<p class="centered"> + <input class="btn_sm" type="submit" name="submit" value="Save Changes"> + <input class="btn_sm" type="reset" name="reset" value="Reset Changes"> +</p> +</form> + +<script type="text/javascript"> + jQuery(function($) { + $('.expand_all').click(function(e) { + e.preventDefault(); + $('.settings_table tbody').each(function() { + $(this).slideDown(); + }) + $('.settings_table h4 span').each(function() { + $(this).html('–'); + }) + }) + $('.collapse_all').click(function(e) { + e.preventDefault(); + $('.settings_table tbody').each(function() { + $(this).slideUp(); + }) + $('.settings_table h4 span').each(function() { + $(this).text('+'); + }) + }) + $('.settings_table h4 a').click(function(e) { + e.preventDefault(); + var parent_elem = $(this).parent().parent().parent().parent().parent(); + $('tbody', parent_elem).slideToggle(); + if($('th span', parent_elem).text() == '+') { + $('th span', parent_elem).html('–') + } else { + $('th span', parent_elem).text('+') + } + }) + }); +</script> + +<?php include "./include/footer.php" ?> diff --git a/include/staff/slaplan.inc.php b/include/staff/slaplan.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..91d896d639d688813389ed0582ecd800beb2a842 --- /dev/null +++ b/include/staff/slaplan.inc.php @@ -0,0 +1,103 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($sla && $_REQUEST['a']!='add'){ + $title='Update SLA Plan'; + $action='update'; + $submit_text='Save Changes'; + $info=$sla->getInfo(); + $info['id']=$sla->getId(); + $qstr.='&id='.$sla->getId(); +}else { + $title='Add New SLA Plan'; + $action='add'; + $submit_text='Add Plan'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; + $info['enable_priority_escalation']=isset($info['enable_priority_escalation'])?$info['enable_priority_escalation']:1; + $info['disable_overdue_alerts']=isset($info['disable_overdue_alerts'])?$info['disable_overdue_alerts']:1; + $qstr.='&a='.urlencode($_REQUEST['a']); +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="slas.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Service Level Agreement</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Tickets are marked overdue on grace period violation.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Grace Period: + </td> + <td> + <input type="text" size="10" name="grace_period" value="<?php echo $info['grace_period']; ?>"> + <em>( in hours )</em> + <span class="error">* <?php echo $errors['grace_period']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180"> + Priority Escalation: + </td> + <td> + <input type="checkbox" name="enable_priority_escalation" value="1" <?php echo $info['enable_priority_escalation']?'checked="checked"':''; ?> > + <strong>Enable</strong> priority escalation on overdue tickets. + </td> + </tr> + <tr> + <td width="180"> + Ticket Overdue Alerts: + </td> + <td> + <input type="checkbox" name="disable_overdue_alerts" value="1" <?php echo $info['disable_overdue_alerts']?'checked="checked"':''; ?> > + <strong>Disable</strong> overdue alerts notices. <em>(Overwrite global setting)</em> + </td> + </tr> + + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="slas.php"'> +</p> +</form> diff --git a/include/staff/slaplans.inc.php b/include/staff/slaplans.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..4ebb46b334e4e2a15b3f63f163824c03accbca29 --- /dev/null +++ b/include/staff/slaplans.inc.php @@ -0,0 +1,120 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT * FROM '.SLA_TABLE.' sla WHERE 1'; +$sortOptions=array('name'=>'sla.name','status'=>'sla.isactive','period'=>'sla.grace_period','date'=>'sla.created','updated'=>'sla.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'sla.name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.SLA_TABLE.' sla '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('slas.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' SLA plans'; +else + $showing='No SLA plans found!'; + +?> + +<div style="width:700;padding-top:5px; float:left;"> + <h2>Service Level Agreements</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="slas.php?a=add" class="Icon newsla">Add New SLA Plan</a></b></div> +<div class="clear"></div> +<form action="slas.php" method="POST" name="slas" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="320"><a <?php echo $name_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="100"><a <?php echo $status_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="130"><a <?php echo $period_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=period">Grace Period (hrs)</a></th> + <th width="120" nowrap><a <?php echo $created_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=created">Date Added</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td> <a href="slas.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a></td> + <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td style="text-align:right;padding-right:35px;"><?php echo $row['grace_period']; ?> </td> + <td> <?php echo Format::db_date($row['created']); ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['slas'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['slas'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['slas'],true)">Toggle</a> + <?php }else{ + echo 'No SLA plans found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected plans?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected plans?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected plans?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..8883397f51fe86c6b46c5207a099ad33ec50ec31 --- /dev/null +++ b/include/staff/staff.inc.php @@ -0,0 +1,289 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$info=array(); +$qstr=''; +if($staff && $_REQUEST['a']!='add'){ + //Editing Department. + $title='Update Staff'; + $action='update'; + $submit_text='Save Changes'; + $passwd_text='To reset the password enter a new one below'; + $info=$staff->getInfo(); + $info['id']=$staff->getId(); + $qstr.='&id='.$staff->getId(); +}else { + $title='Add New Staff'; + $action='create'; + $submit_text='Add Staff'; + $passwd_text='Temp. password required <span class="error"> *</span>'; + //Some defaults for new staff. + $info['change_passwd']=1; + $info['isactive']=1; + $info['isvisible']=1; + $info['isadmin']=0; + $qstr.='&a=add'; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="staff.php?<?php echo $qstr; ?>" method="post" id="save" autocomplete="off"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Staff Account</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em><strong>User Information</strong></em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Username: + </td> + <td> + <input type="text" size="30" name="username" value="<?php echo $info['username']; ?>"> + <span class="error">* <?php echo $errors['username']; ?></span> + </td> + </tr> + + <tr> + <td width="180" class="required"> + First Name: + </td> + <td> + <input type="text" size="30" name="firstname" value="<?php echo $info['firstname']; ?>"> + <span class="error">* <?php echo $errors['firstname']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Last Name: + </td> + <td> + <input type="text" size="30" name="lastname" value="<?php echo $info['lastname']; ?>"> + <span class="error">* <?php echo $errors['lastname']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Email Address: + </td> + <td> + <input type="text" size="30" name="email" value="<?php echo $info['email']; ?>"> + <span class="error">* <?php echo $errors['email']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Phone Number: + </td> + <td> + <input type="text" size="18" name="phone" value="<?php echo $info['phone']; ?>"> + <span class="error"> <?php echo $errors['phone']; ?></span> + Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> + <span class="error"> <?php echo $errors['phone_ext']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Mobile Number: + </td> + <td> + <input type="text" size="18" name="mobile" value="<?php echo $info['mobile']; ?>"> + <span class="error"> <?php echo $errors['mobile']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Account Password</strong>: <?php echo $passwd_text; ?> <span class="error"> <?php echo $errors['temppasswd']; ?></span></em> + </th> + </tr> + <tr> + <td width="180"> + Password: + </td> + <td> + <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>"> + <span class="error"> <?php echo $errors['passwd1']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Confirm Password: + </td> + <td> + <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>"> + <span class="error"> <?php echo $errors['passwd2']; ?></span> + </td> + </tr> + + <tr> + <td width="180"> + Forced Password Change: + </td> + <td> + <input type="checkbox" name="change_passwd" value="0" <?php echo $info['change_passwd']?'checked="checked"':''; ?>> + <strong>Force</strong> password change on next login. + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Staff's Signature</strong>: Optional signature used on outgoing emails. <span class="error"> <?php echo $errors['signature']; ?></span></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="signature" cols="21" rows="5" style="width: 60%;"><?php echo $info['signature']; ?></textarea> + <br><em>Signature is made available as a choice, on ticket reply.</em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Account Status & Settings</strong>: Dept. and assigned group controls access permissions.</em> + </th> + </tr> + <tr> + <td width="180" class="required"> + Account Type: + </td> + <td> + <input type="radio" name="isadmin" value="1" <?php echo $info['isadmin']?'checked="checked"':''; ?>> + <font color="red"><strong>Admin</strong></font> + <input type="radio" name="isadmin" value="0" <?php echo !$info['isadmin']?'checked="checked"':''; ?>><strong>Staff</strong> + <span class="error"> <?php echo $errors['isadmin']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Account Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><strong>Locked</strong> + <span class="error"> <?php echo $errors['isactive']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Assigned Group: + </td> + <td> + <select name="group_id" id="group_id"> + <option value="0">— Select Group —</option> + <?php + $sql='SELECT group_id, group_name, group_enabled as isactive FROM '.GROUP_TABLE.' ORDER BY group_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name,$isactive)=db_fetch_row($res)){ + $sel=($info['group_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s %s</option>',$id,$sel,$name,($isactive?'':' (Disabled)')); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['group_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Primary Department: + </td> + <td> + <select name="dept_id" id="dept_id"> + <option value="0">— Select Department —</option> + <?php + $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' ORDER BY dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $sel=($info['dept_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['dept_id']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + User Default Time Zone: + </td> + <td> + <select name="timezone_id" id="timezone_id"> + <option value="0">— Select Time Zone —</option> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$offset, $tz)=db_fetch_row($res)){ + $sel=($info['timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['timezone_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Limited Access: + </td> + <td> + <input type="checkbox" name="assigned_only" value="1" <?php echo $info['assigned_only']?'checked="checked"':''; ?>>Limit ticket access to ONLY assigned tickets. + </td> + </tr> + <tr> + <td width="180"> + Directory Listing: + </td> + <td> + <input type="checkbox" name="isvisible" value="1" <?php echo $info['isvisible']?'checked="checked"':''; ?>>Show the user on staff's directory + </td> + </tr> + <tr> + <td width="180"> + Vacation Mode: + </td> + <td> + <input type="checkbox" name="onvacation" value="1" <?php echo $info['onvacation']?'checked="checked"':''; ?>> + Staff on vacation mode. (<i>No ticket assignment or alerts</i>) + </td> + </tr> + <?php + //List team assignments. + $sql='SELECT team.team_id, team.name, isenabled FROM '.TEAM_TABLE.' team ORDER BY team.name'; + if(($res=db_query($sql)) && db_num_rows($res)){ ?> + <tr> + <th colspan="2"> + <em><strong>Assigned Teams</strong>: Staff will have access to tickets assigned to a team they belong to regardless of the ticket's department. </em> + </th> + </tr> + <?php + while(list($id,$name,$isactive)=db_fetch_row($res)){ + $checked=($info['teams'] && in_array($id,$info['teams']))?'checked="checked"':''; + echo sprintf('<tr><td colspan=2><input type="checkbox" name="teams[]" value="%d" %s>%s %s</td></tr>', + $id,$checked,$name,($isactive?'':' (Disabled)')); + } + } ?> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes viewable by all admins. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="28" rows="7" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="departments.php"'> +</p> +</form> diff --git a/include/staff/staffmembers.inc.php b/include/staff/staffmembers.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..be8497b97c5030009cac3ff099dd8fadffa00ace --- /dev/null +++ b/include/staff/staffmembers.inc.php @@ -0,0 +1,195 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$qstr=''; +$select='SELECT staff.*,CONCAT_WS(" ",firstname,lastname) as name, grp.group_name, dept.dept_name as dept,count(m.team_id) as teams '; +$from='FROM '.STAFF_TABLE.' staff '. + 'LEFT JOIN '.GROUP_TABLE.' grp ON(staff.group_id=grp.group_id) '. + 'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.dept_id) '. + 'LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.staff_id=staff.staff_id) '; +$where='WHERE 1 '; + +if($_REQUEST['did'] && is_numeric($_REQUEST['did'])) { + $where.=' AND staff.dept_id='.db_input($_REQUEST['did']); + $qstr.='&did='.urlencode($_REQUEST['did']); +} + +if($_REQUEST['gid'] && is_numeric($_REQUEST['gid'])) { + $where.=' AND staff.group_id='.db_input($_REQUEST['gid']); + $qstr.='&gid='.urlencode($_REQUEST['gid']); +} + +if($_REQUEST['tid'] && is_numeric($_REQUEST['tid'])) { + $where.=' AND m.team_id='.db_input($_REQUEST['tid']); + $qstr.='&tid='.urlencode($_REQUEST['tid']); +} + +$sortOptions=array('name'=>'staff.firstname,staff.lastname','username'=>'staff.username','status'=>'isactive', + 'group'=>'grp.group_name','dept'=>'dept.dept_name','created'=>'staff.created','login'=>'staff.lastlogin'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'staff.firstname,staff.lastname'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} + +$order=$order?$order:'ASC'; +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(DISTINCT staff.staff_id) '.$from.' '.$where); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('staff.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$select $from $where GROUP BY staff.staff_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +//echo $query; +?> +<h2>Staff Members</h2> +<div style="width:700; float:left;"> + <form action="staff.php" method="GET" name="filter"> + <input type="hidden" name="a" value="filter" > + <select name="did" id="did"> + <option value="0">— All Department —</option> + <?php + $sql='SELECT dept.dept_id, dept.dept_name,count(staff.staff_id) as users '. + 'FROM '.DEPT_TABLE.' dept '. + 'INNER JOIN '.STAFF_TABLE.' staff ON(staff.dept_id=dept.dept_id) '. + 'GROUP By dept.dept_id HAVING users>0 ORDER BY dept_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name, $users)=db_fetch_row($res)){ + $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users); + } + } + ?> + </select> + <select name="gid" id="gid"> + <option value="0">— All Groups —</option> + <?php + $sql='SELECT grp.group_id, group_name,count(staff.staff_id) as users '. + 'FROM '.GROUP_TABLE.' grp '. + 'INNER JOIN '.STAFF_TABLE.' staff ON(staff.group_id=grp.group_id) '. + 'GROUP BY grp.group_id ORDER BY group_name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name,$users)=db_fetch_row($res)){ + $sel=($_REQUEST['gid'] && $_REQUEST['gid']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users); + } + } + ?> + </select> + <select name="tid" id="tid"> + <option value="0">— All Teams —</option> + <?php + $sql='SELECT team.team_id, team.name, count(member.staff_id) as users FROM '.TEAM_TABLE.' team '. + 'INNER JOIN '.TEAM_MEMBER_TABLE.' member ON(member.team_id=team.team_id) '. + 'GROUP BY team.team_id ORDER BY team.name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name,$users)=db_fetch_row($res)){ + $sel=($_REQUEST['tid'] && $_REQUEST['tid']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users); + } + } + ?> + </select> + + <input type="submit" name="submit" value="Apply"/> + </form> + </div> +<div style="float:right;text-align:right;padding-right:5px;"><b><a href="staff.php?a=add" class="Icon newstaff">Add New Staff</a></b></div> +<div class="clear"></div> +<?php +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing(); +else + $showing='No staff found!'; +?> +<form action="staff.php" method="POST" name="staff" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7px"> </th> + <th width="200"><a <?php echo $name_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="100"><a <?php echo $username_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=username">UserName</a></th> + <th width="100"><a <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="120"><a <?php echo $group_sort; ?>href="staff.php?<?php echo $qstr; ?>&sort=group">Group</a></th> + <th width="150"><a <?php echo $dept_sort; ?>href="staff.php?<?php echo $qstr; ?>&sort=dept">Department</a></th> + <th width="100"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created">Created</a></th> + <th width="145"><a <?php echo $login_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=login">Last Login</a></th> + </tr> + </thead> + <tbody> + <?php + if($res && db_num_rows($res)): + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['staff_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['dept_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['staff_id']; ?>" <?php echo $sel?'checked="checked"':''; ?> + onClick="highLight(this.value,this.checked);"> + <td><a href="staff.php?id=<?php echo $row['staff_id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a> </td> + <td><?php echo $row['username']; ?></td> + <td><?php echo $row['isactive']?'Active':'<b>Locked</b>'; ?> <?php echo $row['onvacation']?'<small>(<i>vacation</i>)</small>':''; ?></td> + <td><a href="groups.php?id=<?php echo $row['group_id']; ?>"><?php echo Format::htmlchars($row['group_name']); ?></a></td> + <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo Format::htmlchars($row['dept']); ?></a></td> + <td><?php echo Format::db_date($row['created']); ?></td> + <td><?php echo Format::db_datetime($row['lastlogin']); ?> </td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="8"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['staff'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['staff'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['staff'],true)">Toggle</a> + <?php }else{ + echo 'No staff members found!'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected users?");'> + + <input class="button" type="submit" name="disable" value="Lock" + onClick=' return confirm("Are you sure you want to LOCK selected users?");'> + + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected users?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..1cec6aff3f253876a77c11d961ca87cba5b3c91f --- /dev/null +++ b/include/staff/syslogs.inc.php @@ -0,0 +1,176 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +if($_REQUEST['type']) { + $qstr.='&type='.urlencode($_REQUEST['type']); +} +$type=null; +switch(strtolower($_REQUEST['type'])){ + case 'error': + $title='Errors'; + $type=$_REQUEST['type']; + break; + case 'warning': + $title='Warnings'; + $type=$_REQUEST['type']; + break; + case 'debug': + $title='Debug logs'; + $type=$_REQUEST['type']; + break; + default: + $type=null; + $title='All logs'; +} + +$qwhere =' WHERE 1'; +//Type +if($type) + $qwhere.=' AND log_type='.db_input($type); + +//dates +$startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0; +$endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0; +if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)){ + $errors['err']='Entered date span is invalid. Selection ignored.'; + $startTime=$endTime=0; +}else{ + if($startTime){ + $qwhere.=' AND created>=FROM_UNIXTIME('.$startTime.')'; + $qstr.='&startDate='.urlencode($_REQUEST['startDate']); + } + if($endTime){ + $qwhere.=' AND created<=FROM_UNIXTIME('.$endTime.')'; + $qstr.='&endDate='.urlencode($_REQUEST['endDate']); + } +} +$sortOptions=array('title'=>'log.title','type'=>'log_type','ip'=>'log.ip_address','date'=>'log.created','created'=>'log.created','updated'=>'log.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'date'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'log.created'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'DESC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$qselect = 'SELECT log.* '; +$qfrom=' FROM '.SYSLOG_TABLE.' log '; +$total=db_count("SELECT count(*) $qfrom $qwhere"); +$page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +//pagenate +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('syslogs.php',$qstr); +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$qselect $qfrom $qwhere ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' '.$title; +else + $showing='No logs found!'; +?> + +<h2>System Logs</h2> +<div id='filter' > + <form action="syslogs.php" method="get"> + <div style="padding-left:2px;"> + <b>Date Span</b>: + From <input class="dp" id="sd" size=15 name="startDate" value="<?php echo Format::htmlchars($_REQUEST['startDate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF> + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('sd')); return false;"><img src='images/cal.png'border=0 alt=""></a> + to + <input class="dp" id="ed" size=15 name="endDate" value="<?php echo Format::htmlchars($_REQUEST['endDate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF > + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('ed')); return false;"><img src='images/cal.png'border=0 alt=""></a> + + Type: + <select name='type'> + <option value="" selected>All</option> + <option value="Error" <?php echo ($type=='Error')?'selected="selected"':''; ?>>Errors</option> + <option value="Warning" <?php echo ($type=='Warning')?'selected="selected"':''; ?>>Warnings</option> + <option value="Debug" <?php echo ($type=='Debug')?'selected="selected"':''; ?>>Debug</option> + </select> + + <input type="submit" Value="Go!" /> + </div> + </form> +</div> +<form action="syslogs.php" method="POST" name="logs" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="320"><a <?php echo $title_sort; ?> href="syslogs.php?<?php echo $qstr; ?>&sort=title">Log Title</a></th> + <th width="100"><a <?php echo $type_sort; ?> href="syslogs.php?<?php echo $qstr; ?>&sort=type">Log Type</a></th> + <th width="200" nowrap><a <?php echo $date_sort; ?>href="syslogs.php?<?php echo $qstr; ?>&sort=date">Log Date</a></th> + <th width="120"><a <?php echo $ip_sort; ?> href="syslogs.php?<?php echo $qstr; ?>&sort=ip">IP Address</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['log_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['log_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['log_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td> <a class="tip" href="log/<?php echo $row['log_id']; ?>"><?php echo Format::htmlchars($row['title']); ?></a></td> + <td><?php echo $row['log_type']; ?></td> + <td> <?php echo Format::db_daydatetime($row['created']); ?></td> + <td><?php echo $row['ip_address']; ?></td> + </tr> + <?php + } //end of while. + endif; ?> + </tbody> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['logs'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['logs'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['logs'],true)">Toggle</a> + <?php }else{ + echo 'No logs found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="delete" value="Delete Selected Entries" + onClick=' return confirm("Are you sure you want to DELETE selected log entries?");'> +</p> +<?php +endif; +?> +</form> diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..7f8c409decba5de347d3cac28b848ce3ad78fa44 --- /dev/null +++ b/include/staff/team.inc.php @@ -0,0 +1,120 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); +$info=array(); +$qstr=''; +if($team && $_REQUEST['a']!='add'){ + //Editing Team + $title='Update Team'; + $action='update'; + $submit_text='Save Changes'; + $info=$team->getInfo(); + $info['id']=$team->getId(); + $qstr.='&id='.$team->getId(); +}else { + $title='Add New Team'; + $action='create'; + $submit_text='Create Team'; + $info['isenabled']=1; + $info['noalerts']=0; + $qstr.='&a='.$_REQUEST['a']; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="teams.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Team</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em><strong>Team Information</strong>: Disabled team won't be availabe for ticket assignment or alerts.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isenabled" value="1" <?php echo $info['isenabled']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isenabled" value="0" <?php echo !$info['isenabled']?'checked="checked"':''; ?>><strong>Disabled</strong> + <span class="error">* </span> + </td> + </tr> + <tr> + <td width="180"> + Team Lead: + </td> + <td> + <select name="lead_id"> + <option value="0">— None —</option> + <option value="" disabled="disabled">Select Team Lead (Optional)</option> + <?php + if($team && ($members=$team->getMembers())){ + foreach($members as $k=>$staff){ + $selected=($info['lead_id'] && $staff->getId()==$info['lead_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$staff->getId(),$selected,$staff->getName()); + } + } + ?> + </select> + <span class="error"> <?php echo $errors['lead_id']; ?></span> + </td> + </tr> + <tr> + <td width="180"> + Assignment Alerts: + </td> + <td> + <input type="checkbox" name="noalerts" value="1" <?php echo $info['noalerts']?'checked="checked"':''; ?> > + <strong>Disable</strong> assignment alerts for this team (<i>overwrite global settings.</i>) + </td> + </tr> + <?php + if($team && ($members=$team->getMembers())){ ?> + <tr> + <th colspan="2"> + <em><strong>Team Members</strong>: To add additional members go to target member's profile </em> + </th> + </tr> + <?php + foreach($members as $k=>$staff){ + echo sprintf('<tr><td colspan=2><span style="width:350px;padding-left:5px; display:block; float:left;"> + <b><a href="staff.php?id=%d">%s</a></span></b> + <input type="checkbox" name="remove[]" value="%d"><i>Remove</i></td></tr>', + $staff->getId(),$staff->getName(),$staff->getId()); + + + } + } ?> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes viewable by all admins. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="signature" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="teams.php"'> +</p> +</form> diff --git a/include/staff/teams.inc.php b/include/staff/teams.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..1aa6dc4ed0554692b67449970caf0d799673f709 --- /dev/null +++ b/include/staff/teams.inc.php @@ -0,0 +1,125 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT team.*,count(m.staff_id) as members,CONCAT_WS(" ",lead.firstname,lead.lastname) as team_lead '. + ' FROM '.TEAM_TABLE.' team '. + ' LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.team_id=team.team_id) '. + ' LEFT JOIN '.STAFF_TABLE.' lead ON(lead.staff_id=team.lead_id) '; +$sql.=' WHERE 1'; +$sortOptions=array('name'=>'team.name','status'=>'team.isenabled','members'=>'members','lead'=>'team_lead','created'=>'team.created'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'team.name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); + +$query="$sql GROUP BY team.team_id ORDER BY $order_by"; +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing="Showing 1-$num of $num teams"; +else + $showing='No teams found!'; + +?> +<div style="width:700;padding-top:5px; float:left;"> + <h2>Teams</h2> + </div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="teams.php?a=add" class="Icon newteam">Add New Team</a></b></div> +<div class="clear"></div> +<form action="teams.php" method="POST" name="teams" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7px"> </th> + <th width="250"><a <?php echo $name_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=name">Team Name</a></th> + <th width="80"><a <?php echo $status_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="80"><a <?php echo $members_sort; ?>href="teams.php?<?php echo $qstr; ?>&sort=members">Members</a></th> + <th width="200"><a <?php echo $lead_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=lead">Team Lead</a></th> + <th width="100"><a <?php echo $created_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=created">Created</a></th> + <th width="130"><a <?php echo $updated_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['team_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + <tr id="<?php echo $row['team_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['team_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td><a href="teams.php?id=<?php echo $row['team_id']; ?>"><?php echo $row['name']; ?></a> </td> + <td> <?php echo $row['isenabled']?'Active':'<b>Disabled</b>'; ?></td> + <td style="text-align:right;padding-right:25px"> + <?php if($row['members']>0) { ?> + <a href="staff.php?tid=<?php echo $row['team_id']; ?>"><?php echo $row['members']; ?></a> + <?php }else{ ?> 0 + <?php } ?> + + </td> + <td><a href="staff.php?id=<?php echo $row['lead_id']; ?>"><?php echo $row['team_lead']; ?> </a></td> + <td><?php echo Format::db_date($row['created']); ?> </td> + <td><?php echo Format::db_datetime($row['updated']); ?> </td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['teams'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['teams'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['teams'],true)">Toggle</a> + <?php }else{ + echo 'No teams found!'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected teams?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected teams?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected teams?");'> +</p> +<?php +endif; +?> + +</form> + diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..15881945e369c2fa541163008b3c9f4c9ce2352f --- /dev/null +++ b/include/staff/template.inc.php @@ -0,0 +1,120 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isadmin()) die('Access Denied'); + +$info=array(); +$qstr=''; +if($template && $_REQUEST['a']!='add'){ + $title='Update Template'; + $action='update'; + $submit_text='Save Changes'; + $info=$template->getInfo(); + $info['id']=$template->getId(); + $qstr.='&id='.$template->getId(); +}else { + $title='Add New Template'; + $action='add'; + $submit_text='Add Template'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:0; + $qstr.='&a='.urlencode($_REQUEST['a']); +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="templates.php?<?php echo $qstr; ?>" method="post" id="save"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Email Template</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Template information.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"> + Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Status: + </td> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong>Active</strong> + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>Disabled + <span class="error">* <?php echo $errors['isactive']; ?></span> + </td> + </tr> + <tr> + <td width="180" class="required"> + Language: + </td> + <td> + <select name="lang_id"> + <option value="en" selected="selected">English (US)</option> + </select> + <span class="error">* <?php echo $errors['lang_id']; ?></span> + </td> + </tr> + <?php + if($template){ ?> + <tr> + <th colspan="2"> + <em><strong>Template Messages</strong>: Click on the message to edit. + <span class="error">* <?php echo $errors['rules']; ?></span></em> + </th> + </tr> + <?php + foreach(Template::message_templates() as $k=>$tpl){ + echo sprintf('<tr><td colspan=2> <strong><a href="templates.php?id=%d&a=manage&tpl=%s">%s</a></strong> - <em>%s</em></td></tr>', + $template->getId(),$k,Format::htmlchars($tpl['name']),Format::htmlchars($tpl['desc'])); + } + }else{ ?> + <tr> + <td width="180" class="required"> + Template To Clone: + </td> + <td> + <select name="tpl_id"> + <option value="0">— Select One ‐</option> + <?php + $sql='SELECT tpl_id,name FROM '.EMAIL_TEMPLATE_TABLE.' ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$name)=db_fetch_row($res)){ + $selected=($info['tpl_id'] && $id==$info['tpl_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['tpl_id']; ?></span> + <em>(select an existing template to copy and edit it thereafter)</em> + </td> + </tr> + <?php } ?> + <tr> + <th colspan="2"> + <em><strong>Admin Notes</strong>: Internal notes. </em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="notes" cols="21" rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="templates.php"'> +</p> +</form> diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..8ce8ca8546ed5932fa5788aec5430400b68e6210 --- /dev/null +++ b/include/staff/templates.inc.php @@ -0,0 +1,127 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$qstr=''; +$sql='SELECT tpl.*,count(dept.tpl_id) as depts '. + 'FROM '.EMAIL_TEMPLATE_TABLE.' tpl '. + 'LEFT JOIN '.DEPT_TABLE.' dept USING(tpl_id) '. + 'WHERE 1 '; +$sortOptions=array('name'=>'tpl.name','status'=>'tpl.isactive','created'=>'tpl.created','updated'=>'tpl.updated'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); +$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name'; +//Sorting options... +if($sort && $sortOptions[$sort]) { + $order_column =$sortOptions[$sort]; +} +$order_column=$order_column?$order_column:'tpl.name'; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) { + $order=$orderWays[strtoupper($_REQUEST['order'])]; +} +$order=$order?$order:'ASC'; + +if($order_column && strpos($order_column,',')){ + $order_column=str_replace(','," $order,",$order_column); +} +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; +$order_by="$order_column $order "; + +$total=db_count('SELECT count(*) FROM '.EMAIL_TEMPLATE_TABLE.' tpl '); +$pagelimit=$thisstaff->getPageLimit(); +$pagelimit=$pagelimit?$pagelimit:PAGE_LIMIT; //true default...if all fails. +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('templates.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); +//Ok..lets roll...create the actual query +$qstr.='&order='.($order=='DESC'?'ASC':'DESC'); +$query="$sql GROUP BY tpl.tpl_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$res=db_query($query); +if($res && ($num=db_num_rows($res))) + $showing=$pageNav->showing().' Templates'; +else + $showing='No templates found!'; + +?> + +<div style="width:700;padding-top:5px; float:left;"> + <h2>Email Templates</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="templates.php?a=add" class="Icon newEmailTemplate">Add New Template</a></b></div> +<div class="clear"></div> +<form action="templates.php" method="POST" name="tpls" onSubmit="return checkbox_checker(this,1,0);"> + <input type="hidden" name="do" value="mass_process" > + <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th width="350"><a <?php echo $name_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=name">Name</a></th> + <th width="100"><a <?php echo $status_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="80"><a <?php echo $inuse_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=inuse">In-Use</a></th> + <th width="120" nowrap><a <?php echo $created_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=created">Date Added</a></th> + <th width="150" nowrap><a <?php echo $updated_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> + </tr> + </thead> + <tbody> + <?php + $total=0; + $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; + if($res && db_num_rows($res)): + $defaultTplId=$cfg->getDefaultTemplateId(); + while ($row = db_fetch_array($res)) { + $inuse=($row['depts'] || $row['tpl_id']==$defaultTplId); + $sel=false; + if($ids && in_array($row['tpl_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + $default=($defaultTplId==$row['tpl_id'])?'<small class="fadded">(System Default)</small>':''; + ?> + <tr id="<?php echo $row['tpl_id']; ?>"> + <td width=7px> + <input type="checkbox" name="ids[]" value="<?php echo $row['tpl_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <td> <a href="templates.php?id=<?php echo $row['tpl_id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a> + <?php echo $default; ?></td> + <td> <?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> + <td> <?php echo ($inuse)?'<b>Yes</b>':'No'; ?></td> + <td> <?php echo Format::db_date($row['created']); ?></td> + <td> <?php echo Format::db_datetime($row['updated']); ?></td> + </tr> + <?php + } //end of while. + endif; ?> + <tfoot> + <tr> + <td colspan="6"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['tpls'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['tpls'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['tpls'],true)">Toggle</a> + <?php }else{ + echo 'No templates found'; + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if($res && $num): //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p class="centered"> + <input class="button" type="submit" name="enable" value="Enable" + onClick=' return confirm("Are you sure you want to ENABLE selected templates?");'> + <input class="button" type="submit" name="disable" value="Disable" + onClick=' return confirm("Are you sure you want to DISABLE selected templates?");'> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected templates?");'> +</p> +<?php +endif; +?> +</form> + diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..a858c5708089f877bdb11d1ce9d58b49375e0046 --- /dev/null +++ b/include/staff/ticket-open.inc.php @@ -0,0 +1,309 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canCreateTickets()) die('Access Denied'); +$info=array(); +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +<form action="tickets.php?a=open" method="post" id="save" enctype="multipart/form-data"> + <input type="hidden" name="do" value="create"> + <input type="hidden" name="a" value="open"> + <h2>Open New Ticket</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>New Ticket</h4> + <em><strong>User Information</strong></em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="160" class="required"> + Email Address: + </td> + <td> + <input type="text" size="30" name="email" value="<?php echo $info['email']; ?>"> + <span class="error">* <?php echo $errors['email']; ?></span> + <?php + if($cfg->notifyONNewStaffTicket()) { ?> + + <input type="checkbox" name="alertuser" <?php echo (!$errors || $info['alertuser'])? 'checked="checked"': ''?>>Send alert to user. + <?php + } ?> + </td> + </tr> + <tr> + <td width="160" class="required"> + Full Name: + </td> + <td> + <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + Phone Number: + </td> + <td> + <input type="text" size="18" name="phone" value="<?php echo $info['phone']; ?>"> + <span class="error"> <?php echo $errors['phone']; ?></span> + Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> + <span class="error"> <?php echo $errors['phone_ext']; ?></span> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Ticket Information</strong>:</em> + </th> + </tr> + <tr> + <td width="160" class="required"> + Ticket Source: + </td> + <td> + <select name="source"> + <option value="" selected >— Select Source —</option> + <option value="Phone" <?php echo ($info['source']=='Phone')?'selected="selected"':''; ?>>Phone</option> + <option value="Email" <?php echo ($info['source']=='Email')?'selected="selected"':''; ?>>Email</option> + <option value="Other" <?php echo ($info['source']=='Other')?'selected="selected"':''; ?>>Other</option> + </select> + <font class="error"><b>*</b> <?=$errors['source']?></font> + </td> + </tr> + <tr> + <td width="160" class="required"> + Department: + </td> + <td> + <select name="deptId"> + <option value="" selected >— Select Department —</option> + <?php + if($depts=Dept::getDepartments()) { + foreach($depts as $id =>$name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['deptId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> + <font class="error"><b>*</b> <?=$errors['deptId']?></font> + </td> + </tr> + + <tr> + <td width="160" class="required"> + Help Topic: + </td> + <td> + <select name="topicId"> + <option value="" selected >— Select Help Topic —</option> + <?php + if($topics=Topic::getHelpTopics()) { + foreach($topics as $id =>$name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['topicId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> + <font class="error"><b>*</b> <?=$errors['topicId']?></font> + </td> + </tr> + <tr> + <td width="160" class="required"> + Subject: + </td> + <td> + <input type="text" name="subject" size="35" value="<?=$info['subject']?>"> + <font class="error">* <?=$errors['subject']?></font> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Issue summary</strong>: Detailed summary of the reason(s) of opening the ticket. <font class="error">* <?=$errors['issue']?></font></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="issue" cols="21" rows="8" style="width:80%;"><?php echo $info['issue']; ?></textarea> + <br><em>The user will be able to see the summary and any associated responses.</em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Response</strong>: Optional response to the above issue.</em> + </th> + </tr> + <tr> + <td colspan=2> + <?php + if(($cannedResponses=Canned::getCannedResponses())) { + ?> + <div> + Canned Response: + <select id="cannedResp" name="cannedResp"> + <option value="0" selected="selected">— Select a canned response —</option> + <?php + foreach($cannedResponses as $id =>$title) { + echo sprintf('<option value="%d">%s</option>',$id,$title); + } + ?> + </select> + + <label><input type='checkbox' value='1' name="append" id="append" checked="checked">Append</label> + </div> + <?php + } ?> + <textarea name="response" id="response" cols="21" rows="8" style="width:80%;"><?php echo $info['response']; ?></textarea> + <?php + if($cfg->allowAttachments()) { ?> + <br><em><b>Attachments:</b> Response required when files are attached.</em> + <div id="canned_attachments"> + <?php + if($info['cannedattachments']) { + foreach($info['cannedattachments'] as $k=>$id) { + if(!($file=AttachmentFile::lookup($id))) continue; + $hash=$file->getHash().md5($file->getId().session_id().$file->getHash()); + echo sprintf('<label><input type="checkbox" name="cannedattachments[]" id="f%d" value="%d" checked="checked"> + <a href="file.php?h=%s">%s</a> </label> ', + $file->getId(), $file->getId() , $hash, $file->getName()); + } + } + ?> + </div> + <div id="uploads"></div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + <? + } ?> + </td> + </tr> + + <tr> + <th colspan="2"> + <em><strong>Internal Note</strong>: Optional internal note (recommended on assignment) <font class="error"> <?php echo $errors['note'];?></font></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="note" cols="21" rows="6" style="width:80%;"><?php echo $info['note']; ?></textarea> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Ticket Options</strong>: Due date, when set, overwrites SLA grace period.</em> + </th> + </tr> + <tr> + <td width="160"> + Due Date: + </td> + <td> + <input id="duedate" name="duedate" value="<?php echo Format::htmlchars($info['duedate']); ?>" size="10" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF> + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('duedate')); return false;"><img src='images/cal.png'border=0 alt=""></a> + + <?php + $min=$hr=null; + if($info['time']) + list($hr,$min)=explode(':',$info['time']); + echo Misc::timeDropdown($hr,$min,'time'); + ?> + <font class="error"> <?=$errors['duedate']?> <?php echo $errors['time']; ?></font> + <em>Time is based on your time zone (GM <?php echo $thisstaff->getTZoffset(); ?>)</em> + </td> + </tr> + <tr> + <td width="160"> + Priority: + </td> + <td> + <select name="priorityId"> + <option value="0" selected >— System Default —</option> + <?php + if($priorities=Priority::getPriorities()) { + foreach($priorities as $id =>$name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['priorityId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> + <font class="error"> <?=$errors['priorityId']?></font> + </td> + </tr> + <?php + if($thisstaff->canAssignTickets()) { ?> + <tr> + <td width="160">Assign To:</td> + <td> + <select id="assignId" name="assignId"> + <option value="0" selected="selected">— Select Staff Member OR a Team —</option> + <?php + if(($users=Staff::getAvailableStaffMembers())) { + echo '<OPTGROUP label="Staff Members ('.count($users).')">'; + foreach($users as $id => $name) { + $k="s$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + + if(($teams=Team::getActiveTeams())) { + echo '<OPTGROUP label="Teams ('.count($teams).')">'; + foreach($teams as $id => $name) { + $k="t$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + ?> + </select> <span class='error'> <?php echo $errors['assignId']; ?></span> + </td> + </tr> + <?php + } ?> + <?php + if($thisstaff->canCloseTickets()) { ?> + <tr> + <td width="160"> + Ticket Status: + </td> + <td> + <input type="checkbox" name="ticket_state" value="closed" <?php echo $info['ticket_state']?'checked="checked"':''; ?>> + <b>Close On Response</b> <em>(Only applicable if response is entered)</em> + </td> + </tr> + <?php + } ?> + <tr> + <td>Signature:</td> + <td> + <?php + $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); + ?> + <label><input type="radio" name="signature" value="none" checked="checked"> None</label> + <?php + if($thisstaff->getSignature()) {?> + <label><input type="radio" name="signature" value="mine" + <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> + <?php + } ?> + <label><input type="radio" name="signature" value="dept" + <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> + Dept. Signature (if set)</label> + <span style="padding-left:25px;"> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input type="submit" name="submit" value="Open"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="departments.php"'> +</p> +</form> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..f158a387b518a9996af30f6669016e936f7bdecb --- /dev/null +++ b/include/staff/ticket-view.inc.php @@ -0,0 +1,579 @@ +<?php +//Note that ticket obj is initiated in tickets.php. +if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path'); + +//Make sure the staff is allowed to access the page. +if(!@$thisstaff->isStaff() || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied'); + +//Re-use the post info on error...savekeyboards.org (Why keyboard? -> some people care about objects than users!!) +$info=($_POST && $errors)?Format::input($_POST):array(); + +//Auto-lock the ticket if locking is enabled.. If already locked by the user then it simply renews. +if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime())) + $warn.='Unable to obtain a lock on the ticket'; + +//Get the goodies. +$dept = $ticket->getDept(); //Dept +$staff = $ticket->getStaff(); //Assigned or closed by.. +$team = $ticket->getTeam(); //Assigned team. +$lock = $ticket->getLock(); //Ticket lock obj +$id = $ticket->getId(); //Ticket ID. + +//Useful warnings and errors the user might want to know! +if($ticket->isAssigned() && $staff->getId()!=$thisstaff->getId()) + $warn.=' <span class="Icon assignedTicket">Ticket is assigned to '.$ticket->getAssignee().'</span>'; +if(!$errors['err'] && ($lock && $lock->getStaffId()!=$thisstaff->getId())) + $errors['err']='This ticket is currently locked by '.$lock->getStaffName(); +if(!$errors['err'] && ($emailBanned=EmailFilter::isBanned($ticket->getEmail()))) + $errors['err']='Email is in banlist! Must be removed before any reply/response'; + +$unbannable=($emailBanned) ? BanList::includes($ticket->getEmail()) : false; + +if($ticket->isOverdue()) + $warn.=' <span class="Icon overdueTicket">Marked overdue!</span>'; + +?> +<table width="910" cellpadding="2" cellspacing="0" border="0"> + <tr> + <td width="50%"> + <h2>Ticket #<?php echo $ticket->getExtId(); ?> + <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload" class="reload">Reload</a></h2> + </td> + <td width="50%" class="right_align"> + <a href="#" title="Print Ticket" class="print">Print Ticket</a> + <a href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit" title="Edit Ticket" class="edit">Edit Ticket</a> + </td> + </tr> +</table> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> + <tr> + <td width="50"> + <table border="0" cellspacing="" cellpadding="4" width="100%"> + <tr> + <th width="100">Status:</th> + <td><?php echo ucfirst($ticket->getStatus()); ?></td> + </tr> + <tr> + <th>Priority:</th> + <td><?php echo $ticket->getPriority(); ?></td> + </tr> + <tr> + <th>Department:</th> + <td><?php echo Format::htmlchars($ticket->getDeptName()); ?></td> + </tr> + <tr> + <th>Create Date:</th> + <td><?php echo Format::db_datetime($ticket->getCreateDate()); ?></td> + </tr> + </table> + </td> + <td width="50%"> + <table border="0" cellspacing="" cellpadding="4" width="100%"> + <tr> + <th width="100">Name:</th> + <td><?php echo Format::htmlchars($ticket->getName()); ?></td> + </tr> + <tr> + <th>Email:</th> + <td> + <?php + echo $ticket->getEmail(); + if(($related=$ticket->getRelatedTicketsCount())) { + echo sprintf(' <a href="tickets.php?a=search&query=%s" title="Related Tickets">(<b>%d</b>)</a>', + urlencode($ticket->getEmail()),$related); + + } + ?> + </td> + </tr> + <tr> + <th>Phone:</th> + <td><?php echo $ticket->getPhoneNumber(); ?></td> + </tr> + <tr> + <th>Source:</th> + <td><?php echo Format::htmlchars($ticket->getSource()); ?></td> + </tr> + </table> + </td> + </tr> +</table> +<br> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> + <tr> + <td width="50%"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <tr> + <th width="100">Assigned To:</th> + <td><?php echo Format::htmlchars($ticket->getAssignee()); ?></td> + </tr> + <tr> + <th nowrap>Last Response:</th> + <td><?php echo Format::db_datetime($ticket->getLastRespDate()); ?></td> + </tr> + <?php + if($ticket->isOpen()){ ?> + <tr> + <th>Due Date:</th> + <td><?php echo Format::db_datetime($ticket->getDueDate()); ?></td> + </tr> + <?php + }else { ?> + <tr> + <th>Close Date:</th> + <td><?php echo Format::db_datetime($ticket->getCloseDate()); ?></td> + </tr> + <?php + } + ?> + </table> + </td> + <td width="50%"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <tr> + <th width="100">Subject:</th> + <td><?php echo Format::htmlchars(Format::truncate($ticket->getSubject(),200)); ?></td> + </tr> + <tr> + <th>Help Topic:</th> + <td><?php echo Format::htmlchars($ticket->getHelpTopic()); ?></td> + </tr> + <tr> + <th nowrap>Last Message:</th> + <td><?php echo Format::db_datetime($ticket->getLastMsgDate()); ?></td> + </tr> + </table> + </td> + </tr> +</table> +<div class="clear" style="padding-bottom:10px;"></div> +<ul id="threads"> + <li><a class="active" id="toggle_ticket_thread" href="#">Ticket Thread (<?php echo $ticket->getThreadCount(); ?>)</a></li> + <li><a id="toggle_notes" href="#">Internal Notes (<?php echo $ticket->getNumNotes(); ?>)</a></li> +</ul> + +<div id="ticket_notes"> + <?php + /* Internal Notes */ + if($ticket->getNumNotes() && ($notes=$ticket->getNotes())) { + foreach($notes as $note) { + + ?> + <table class="note" cellspacing="0" cellpadding="1" width="940" border="0"> + <tr> + <th width="640"> + <?php + echo sprintf('%s <em>posted by <b>%s</b></em>', + Format::htmlchars($note['title']), + Format::htmlchars($note['source'])); + ?> + </th> + <th class="date" width="300"><?php echo Format::db_datetime($note['created']); ?></th> + </tr> + <tr> + <td colspan="2"> + <?php echo Format::htmlchars($note['note']); ?> + </td> + </tr> + <?php + if($note['attachments'] && ($links=$ticket->getAttachmentsLinks($note['note_id'],'N'))) {?> + <tr> + <td class="info" colspan="2"><?php echo $links; ?></td> + </tr> + <?php + }?> + </table> + <?php + } + } else { + echo "<p>No internal notes found.</p>"; + }?> +</div> +<div id="ticket_thread"> + <?php + /* -------- Messages & Responses -------------*/ + if($ticket->getThreadCount() && ($messages = $ticket->getMessages())) { + foreach($messages as $message) {?> + <table class="message" cellspacing="0" cellpadding="1" width="940" border="0"> + <tr><th><?php echo Format::db_datetime($message['created']); ?></th></tr> + <tr><td><?php echo Format::display($message['message']); ?></td></tr> + <?php + if($message['attachments'] && ($links=$ticket->getAttachmentsLinks($message['msg_id'],'M'))) {?> + <tr> + <td class="info"><?php echo $links; ?></td> + </tr> + <?php + }?> + </table> + <?php + /* --------- Responses ------------ */ + if($message['responses'] && ($responses=$ticket->getResponses($message['msg_id']))) { + foreach($responses as $resp) {?> + <table class="response" cellspacing="0" cellpadding="1" width="100%" border="0"> + <tr> + <th><?php echo Format::db_datetime($resp['created']); ?> - <?php echo Format::htmlchars($resp['staff_name']); ?></th> + </tr> + <tr> + <td><?php echo Format::display($resp['response']); ?></td> + </tr> + <?php + if($resp['attachments'] && ($links=$ticket->getAttachmentsLinks($resp['response_id'],'R'))) {?> + <tr> + <td class="info"><?php echo $links; ?></td> + </tr> + <?php + }?> + </table> + <?php + } + } + $msgId=$message['msg_id']; + } + } else { + echo '<p>Error fetching ticket thread - get technical help.</p>'; + }?> +</div> +<div class="clear" style="padding-bottom:10px;"></div> +<?php if($errors['err']) { ?> + <div id="msg_error"><?php echo $errors['err']; ?></div> +<?php }elseif($msg) { ?> + <div id="msg_notice"><?php echo $msg; ?></div> +<?php }elseif($warn) { ?> + <div id="msg_warning"><?php echo $warn; ?></div> +<?php } ?> + +<div id="response_options"> + <ul> + <li><a id="reply_tab" href="#reply">Post Reply</a></li> + <li><a id="note_tab" href="#note">Post Internal Note</a></li> + <?php + if($thisstaff->canTransferTickets()) { ?> + <li><a id="transfer_tab" href="#transfer">Dept. Transfer</a></li> + <?php + } + + if($thisstaff->canAssignTickets()) { ?> + <li><a id="assign_tab" href="#assign"><?php echo $ticket->isAssigned()?'Reassign Ticket':'Assign Ticket'; ?></a></li> + <?php + } ?> + </ul> + + <form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="msgId" value="<?php echo $msgId; ?>"> + <input type="hidden" name="a" value="reply"> + <span class="error"></span> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['response']; ?></td> + </tr> + <?php + if(($cannedResponses=Canned::responsesByDeptId($ticket->getDeptId()))) {?> + <tr> + <td width="160"> + <label> </label> + </td> + <td width="765"> + <select id="cannedResp" name="cannedResp"> + <option value="0" selected="selected">Select a canned response</option> + <?php + foreach($cannedResponses as $id =>$title) { + echo sprintf('<option value="%d">%s</option>',$id,$title); + } + ?> + </select> + + <label><input type='checkbox' value='1' name="append" id="append" checked="checked"> Append</label> + </td> + </tr> + <?php + }?> + <tr> + <td width="160"> + <label><strong>Response:</strong></label> + </td> + <td width="765"> + <textarea name="response" id="response" cols="50" rows="9" wrap="soft"><?php echo $info['response']; ?></textarea> + </td> + </tr> + <?php + if($cfg->allowAttachments()) { ?> + <tr> + <td width="160"> + <label for="attachment">Attachments:</label> + </td> + <td width="765" id="reply_form_attachments" class="attachments"> + <div id="canned_attachments"> + </div> + <div id="uploads"> + </div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + </td> + </tr> + <?php + }?> + <tr> + <td width="160"> + <label for="signature" class="left">Signature:</label> + </td> + <td width="765"> + <?php + $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); + ?> + <label><input type="radio" name="signature" value="none" checked="checked"> None</label> + <?php + if($thisstaff->getSignature()) {?> + <label><input type="radio" name="signature" value="mine" + <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> + <?php + } ?> + <?php + if($dept && $dept->canAppendSignature()) { ?> + <label><input type="radio" name="signature" value="dept" + <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> + Dept. Signature (<?php echo Format::htmlchars($dept->getName()); ?>)</label> + <?php + } ?> + </td> + </tr> + <?php + if($ticket->isClosed() || $thisstaff->canCloseTickets()) { ?> + <tr> + <td width="160"> + <label><strong>Ticket Status:</strong></label> + </td> + <td width="765"> + <?php + $statusChecked=isset($info['reply_ticket_status'])?'checked="checked"':''; + if($ticket->isClosed()) { ?> + <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Open" + <?php echo $statusChecked; ?>> Reopen on Reply</label> + <?php + } elseif($thisstaff->canCloseTickets()) { ?> + <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Closed" + <?php echo $statusChecked; ?>> Close on Reply</label> + <?php + } ?> + </td> + </tr> + <?php + } ?> + </div> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Post Reply"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <form id="note" action="tickets.php?id=<?php echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data"> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="postnote"> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['note']; ?></td> + </tr> + <tr> + <td width="160"> + <label><strong>Title:</strong></label> + </td> + <td width="765"> + <input type="text" name="title" id="title" size="45" value="<?php echo $info['title']; ?>" > + <span class="error">* <?php echo $errors['title']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + <label><strong>Note:</strong></label> + </td> + <td width="765"> + <div><span class="faded">Internal note details</span> + <span class="error">* <?php echo $errors['internal_note']; ?></span></div> + <textarea name="internal_note" id="internal_note" cols="50" rows="9" wrap="soft" + style="width:600px"><?php echo $info['internal_note']; ?></textarea> + </td> + </tr> + + <?php + if($cfg->allowAttachments()) { ?> + <tr> + <td width="160"> + <label for="attachment">Attachments:</label> + </td> + <td width="765" class="attachments"> + <div class="uploads"> + </div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + </td> + </tr> + <?php + } + ?> + <tr> + <td width="160"> + <label>Ticket Status:</label> + </td> + <td width="765"> + <?php + $statusChecked=isset($info['note_ticket_state'])?'checked="checked"':''; + if($ticket->isClosed()){ ?> + <label><input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="open" + <?php echo $statusChecked; ?>> Reopen Ticket</label> + <?php + } elseif(0 && $thisstaff->canCloseTickets()) { ?> + <label><input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Closed" + <?php echo $statusChecked; ?>> Close Ticket</label> + <?php + } elseif($ticket->isAnswered()) { ?> + <label> + <input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Unanswered" + <?php echo $statusChecked; ?>> + Mark Unanswered + </label> + <?php + } else { ?> + <label> + <input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Answered" + <?php echo $statusChecked; ?>> + Mark Answered + </label> + <?php + } ?> + </td> + </tr> + </div> + </table> + + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Post Note"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + if($thisstaff->canTransferTickets()) { ?> + <form id="transfer" action="tickets.php?id=<?php echo $ticket->getId(); ?>#transfer" name="transfer" method="post" enctype="multipart/form-data"> + <input type="hidden" name="ticket_id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="transfer"> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['transfer']; ?></td> + </tr> + <tr> + <td width="160"> + <label for="deptId"><strong>Department:</strong></label> + </td> + <td width="765"> + <select id="deptId" name="deptId"> + <option value="0" selected="selected">— Select Target Department —</option> + <?php + if($depts=Dept::getDepartments()) { + foreach($depts as $id =>$name) { + if($id==$ticket->getDeptId()) continue; + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['deptId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> <span class='error'>* <?php echo $errors['deptId']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + <label><strong>Comments:</strong></label> + </td> + <td width="765"> + <span class="faded">Enter reasons for the transfer.</span> + <span class="error">* <?php echo $errors['transfer_message']; ?></span><br> + <textarea name="transfer_message" id="transfer_message" + cols="80" rows="7" wrap="soft"><?php echo $info['transfer_message']; ?></textarea> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Transfer"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + } ?> + <?php + if($thisstaff->canAssignTickets()) { ?> + <form id="assign" action="tickets.php?id=<?php echo $ticket->getId(); ?>#assign" name="assign" method="post" enctype="multipart/form-data"> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="assign"> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="160"> </td> + <td> + <?php + if($ticket->isAssigned()) + echo sprintf('<em>Ticket is currently assigned to <b>%s</b></em>',$ticket->getAssignee()); + ?> + </td> + </tr> + <tr> + <td width="160"> + <label for="assignId"><strong>Assignee:</strong></label> + </td> + <td width="765"> + <select id="assignId" name="assignId"> + <option value="0" selected="selected">— Select Staff Member OR a Team —</option> + <?php + $sid=$tid=0; + if(($users=Staff::getAvailableStaffMembers())) { + echo '<OPTGROUP label="Staff Members ('.count($users).')">'; + $staffId=$ticket->isAssigned()?$ticket->getStaffId():0; + foreach($users as $id => $name) { + if($staffId && $staffId==$id) + $name.=' (Assigned)'; + + $k="s$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + + if(($teams=Team::getActiveTeams())) { + echo '<OPTGROUP label="Teams ('.count($teams).')">'; + $teamId=(!$sid && $ticket->isAssigned())?$ticket->getTeamId():0; + foreach($teams as $id => $name) { + if($teamId && $teamId==$id) + $name.=' (Assigned)'; + + $k="t$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + ?> + </select> <span class='error'>* <?php echo $errors['assignId']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + <label><strong>Comments:</strong><span class='error'> </span></label> + </td> + <td width="765"> + <span class="faded">Enter reasons for the assignment or instructions.</span> + <span class="error">* <?php echo $errors['assign_message']; ?></span><br> + <textarea name="assign_message" id="assign_message" cols="80" rows="7" wrap="soft"><?php echo $info['assign_message']; ?></textarea> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="<?php echo $ticket->isAssigned()?'Reassign':'Assign'; ?>"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + } ?> +</div> +<script type="text/javascript" src="js/ticket.js"></script> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..dad492a18f83dd64ed22f94de6a662de5e46ae45 --- /dev/null +++ b/include/staff/tickets.inc.php @@ -0,0 +1,528 @@ +<?php +if(!defined('OSTSCPINC') || !$thisstaff || !@$thisstaff->isStaff()) die('Access Denied'); + +$qstr='&'; //Query string collector +if($_REQUEST['status']) { //Query string status has nothing to do with the real status used below; gets overloaded. + $qstr.='status='.urlencode($_REQUEST['status']); +} + +//See if this is a search +$search=($_REQUEST['a']=='search'); +$searchTerm=''; +//make sure the search query is 3 chars min...defaults to no query with warning message +if($search) { + $searchTerm=$_REQUEST['query']; + if( ($_REQUEST['query'] && strlen($_REQUEST['query'])<3) + || (!$_REQUEST['query'] && isset($_REQUEST['basic_search'])) ){ //Why do I care about this crap... + $search=false; //Instead of an error page...default back to regular query..with no search. + $errors['err']='Search term must be more than 3 chars'; + $searchTerm=''; + } +} +$showoverdue=$showanswered=$showassigned=false; +$staffId=0; //Nothing for now...TODO: Allow admin and manager to limit tickets to single staff level. +//show Assigned To column, if enabled. Admins and managers can overwrite system settings! +$showassigned=(($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets()) && !$search); + +//Get status we are actually going to use on the query...making sure it is clean! +$status=null; +switch(strtolower($_REQUEST['status'])){ //Status is overloaded + case 'open': + $status='open'; + break; + case 'closed': + $status='closed'; + $showassigned=false; + break; + case 'overdue': + $status='open'; + $showoverdue=true; + $results_type='Overdue Tickets'; + break; + case 'assigned': + $status='open'; + $staffId=$thisstaff->getId(); + break; + case 'answered': + $status='open'; + $showanswered=true; + $results_type='Answered Tickets'; + break; + default: + if(!$search) + $status='open'; +} + +$qwhere =''; +/* + STRICT DEPARTMENTS BASED PERMISSION! + User can also see tickets assigned to them regardless of the ticket's dept. +*/ + +$depts=$thisstaff->getDepts(); +$qwhere =' WHERE ( ' + .' ticket.dept_id IN ('.($depts?implode(',',$depts):0).') OR ticket.staff_id='.$thisstaff->getId(); +if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) + $qwhere.=' OR ticket.team_id IN('.implode(',',array_filter($teams)).') '; +$qwhere .= ' )'; + +//STATUS +if($status){ + $qwhere.=' AND status='.db_input(strtolower($status)); +} + +//Overloaded sub-statuses - you've got to just have faith! +if($staffId && ($staffId==$thisstaff->getId())) { //Staff's assigned tickets. + $results_type='Assigned Tickets'; + $qwhere.=' AND ticket.staff_id='.db_input($staffId); + $showassigned=false; //My tickets...already assigned to the staff. +}elseif($showoverdue) { //overdue + $qwhere.=' AND isoverdue=1 '; +}elseif($showanswered) { ////Answered + $qwhere.=' AND isanswered=1 '; +}elseif(!$search && !$cfg->showAnsweredTickets() && !strcasecmp($status,'open')) { + $qwhere.=' AND isanswered=0 '; +} + +//******* Showing assigned tickets? (don't confuse it with show assigned To column). F'it it's confusing - just trust me! ***/ +if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets()) && strcasecmp($status,'closed')) + $sql.=' AND (ticket.staff_id=0 OR ticket.staff_id='.db_input($thisstaff->getId()).') '; + +//Search?? Somebody...get me some coffee +$deep_search=false; +if($search): + $qstr.='&a='.urlencode($_REQUEST['a']); + $qstr.='&t='.urlencode($_REQUEST['t']); + if(isset($_REQUEST['advance_search'])){ //advance search box! + $qstr.='&advance_search=Search'; + } + + //query + if($searchTerm){ + $qstr.='&query='.urlencode($searchTerm); + $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes. + if(is_numeric($searchTerm)){ + $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'"; + }elseif(strpos($searchTerm,'@') && Validator::is_email($searchTerm)){ //pulling all tricks! + $qwhere.=" AND ticket.email='$queryterm'"; + }else{//Deep search! + //This sucks..mass scan! search anything that moves! + + $deep_search=true; + if($_REQUEST['stype'] && $_REQUEST['stype']=='FT') { //Using full text on big fields. + $qwhere.=" AND ( ticket.email LIKE '%$queryterm%'". + " OR ticket.name LIKE '%$queryterm%'". + " OR ticket.subject LIKE '%$queryterm%'". + " OR note.title LIKE '%$queryterm%'". + " OR MATCH(message.message) AGAINST('$queryterm')". + " OR MATCH(response.response) AGAINST('$queryterm')". + " OR MATCH(note.note) AGAINST('$queryterm')". + ' ) '; + }else{ + $qwhere.=" AND ( ticket.email LIKE '%$queryterm%'". + " OR ticket.name LIKE '%$queryterm%'". + " OR ticket.subject LIKE '%$queryterm%'". + " OR message.message LIKE '%$queryterm%'". + " OR response.response LIKE '%$queryterm%'". + " OR note.note LIKE '%$queryterm%'". + " OR note.title LIKE '%$queryterm%'". + ' ) '; + } + } + } + //department + if($_REQUEST['dept'] && in_array($_REQUEST['dept'],$thisstaff->getDepts())) { + //This is dept based search..perm taken care above..put the sucker in. + $qwhere.=' AND ticket.dept_id='.db_input($_REQUEST['dept']); + $qstr.='&dept='.urlencode($_REQUEST['dept']); + } + //Teams + if($_REQUEST['team'] && ($thisuser->isadmin() || in_array($_REQUEST['team'],$thisuser->getTeams()))) { + $qwhere.=' AND ticket.team_id='.db_input($_REQUEST['team']); + $qstr.='&team='.urlencode($_REQUEST['team']); + } + + //dates + $startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0; + $endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0; + if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)){ + $errors['err']='Entered date span is invalid. Selection ignored.'; + $startTime=$endTime=0; + }else{ + //Have fun with dates. + if($startTime){ + $qwhere.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')'; + $qstr.='&startDate='.urlencode($_REQUEST['startDate']); + + } + if($endTime){ + $qwhere.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')'; + $qstr.='&endDate='.urlencode($_REQUEST['endDate']); + } +} + +endif; + +$sortOptions=array('date'=>'ticket.created','ID'=>'ticketID','pri'=>'priority_urgency','name'=>'ticket.name', + 'subj'=>'ticket.subject','status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff'); +$orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); + +//Sorting options... +$order_by=$order=null; +if($_REQUEST['sort'] && $sortOptions[$_REQUEST['sort']]) + $order_by =$sortOptions[$_REQUEST['sort']]; + +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) + $order=$orderWays[strtoupper($_REQUEST['order'])]; + +if(!$order_by && $showanswered) { + $order_by='ticket.lastresponse, ticket.created'; //No priority sorting for answered tickets. +}elseif(!$order_by && !strcasecmp($status,'closed')){ + $order_by='ticket.closed, ticket.created'; //No priority sorting for closed tickets. +} +$order_by =$order_by?$order_by:'priority_urgency,effective_date,ticket.created'; +$order=$order?$order:'DESC'; + +if($order_by && strpos($order_by,',')) + $order_by=str_replace(','," $order,",$order_by); + +$sort=$_REQUEST['sort']?strtolower($_REQUEST['sort']):'urgency'; //Urgency is not on display table. +$x=$sort.'_sort'; +$$x=' class="'.strtolower($order).'" '; + +if($_GET['limit']) + $qstr.='&limit='.urlencode($_GET['limit']); + +$qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' + .' ,ticket.subject,ticket.name,ticket.email,dept_name ' + .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created,pri.* '; + +$qfrom=' FROM '.TICKET_TABLE.' ticket '. + ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '; + +if($search && $deep_search) { + $sjoin=' LEFT JOIN '.TICKET_MESSAGE_TABLE.' message ON (ticket.ticket_id=message.ticket_id )' + .' LEFT JOIN '.TICKET_RESPONSE_TABLE.' response ON (ticket.ticket_id=response.ticket_id )' + .' LEFT JOIN '.TICKET_NOTE_TABLE.' note ON (ticket.ticket_id=note.ticket_id ) '; +} + +$qgroup=' GROUP BY ticket.ticket_id'; +//get ticket count based on the query so far.. +$total=db_count("SELECT count(DISTINCT ticket.ticket_id) $qfrom $sjoin $qwhere"); +//pagenate +$pagelimit=($_GET['limit'] && is_numeric($_GET['limit']))?$_GET['limit']:$thisstaff->getPageLimit(); +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total,$page,$pagelimit); +$pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); + +//ADD attachment,priorities, lock and other crap +$qselect.=' ,count(attach.attach_id) as attachments ' + .' ,count(DISTINCT message.msg_id) as messages ' + .' ,count(DISTINCT response.response_id) as responses ' + .' ,count(DISTINCT note.note_id) as notes ' + .' ,IF(ticket.reopened is NULL,IF(ticket.lastmessage is NULL,ticket.created,ticket.lastmessage),ticket.reopened) as effective_date ' + .' ,CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team ' + .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '; + +$qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.priority_id) ' + .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() + AND tlock.staff_id!='.db_input($thisstaff->getId()).') ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TICKET_MESSAGE_TABLE.' message ON (ticket.ticket_id=message.ticket_id) ' + .' LEFT JOIN '.TICKET_RESPONSE_TABLE.' response ON (ticket.ticket_id=response.ticket_id) ' + .' LEFT JOIN '.TICKET_NOTE_TABLE.' note ON (ticket.ticket_id=note.ticket_id ) ' + .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) ' + .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) '; + +$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +//echo $query; +$res = db_query($query); +$showing=db_num_rows($res)?$pageNav->showing():""; +if(!$results_type) { + $results_type=($search)?'Search Results':ucfirst($status).' Tickets'; +} +$negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. + +$basic_display=!isset($_REQUEST['advance_search'])?true:false; + +//YOU BREAK IT YOU FIX IT. +?> +<!-- SEARCH FORM START --> +<div id='basic' style="display:<?php echo $basic_display?'block':'none'; ?>"> + <form action="tickets.php" method="get"> + <input type="hidden" name="a" value="search"> + <table> + <tr> + <td><input type="text" id="query" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"></td> + <td><input type="submit" name="basic_search" class="button" value="Search"></td> + </tr> + </table> + </form> +</div> +<div id='advance' style="display:<?php echo $basic_display?'none':'block'; ?>"> + <form action="tickets.php" method="get"> + <input type="hidden" name="a" value="search"> + <table> + <tr> + <td>Query: </td><td><input type="text" id="query" name="query" value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"></td> + <td>Dept:</td> + <td><select name="dept"><option value=0>All Departments</option> + <?php + //Showing only departments the user has access to... + $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE; + if(!$thisstaff->isadmin()) + $sql.=' WHERE dept_id IN ('.implode(',',$thisstaff->getDepts()).')'; + + $depts= db_query($sql); + while (list($deptId,$deptName) = db_fetch_row($depts)){ + $selected = ($_GET['dept']==$deptId)?'selected':''; ?> + <option value="<?php echo $deptId; ?>"<?php echo $selected; ?>><?php echo $deptName; ?></option> + <?php + } ?> + </select> + </td> + <td>Status is:</td><td> + + <select name="status"> + <option value='any' selected >Any status</option> + <option value="open" <?php echo !strcasecmp($_REQUEST['status'],'Open')?'selected':''; ?>>Open</option> + <option value="overdue" <?php echo !strcasecmp($_REQUEST['status'],'overdue')?'selected':''; ?>>Overdue</option> + <option value="closed" <?php echo !strcasecmp($_REQUEST['status'],'Closed')?'selected':''; ?>>Closed</option> + </select> + </td> + </tr> + </table> + <div> + Date Span: + From <input id="sd" name="startDate" value="<?php echo Format::htmlchars($_REQUEST['startDate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF> + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('sd')); return false;"><img src='images/cal.png'border=0 alt=""></a> + to + <input id="ed" name="endDate" value="<?php echo Format::htmlchars($_REQUEST['endDate']); ?>" + onclick="event.cancelBubble=true;calendar(this);" autocomplete=OFF > + <a href="#" onclick="event.cancelBubble=true;calendar(getObj('ed')); return false;"><img src='images/cal.png'border=0 alt=""></a> + + </div> + <table> + <tr> + <td>Type:</td> + <td> + <select name="stype"> + <option value="LIKE" <?php echo (!$_REQUEST['stype'] || $_REQUEST['stype'] == 'LIKE') ?'selected':''; ?>>Scan (%)</option> + <option value="FT"<?php echo $_REQUEST['stype'] == 'FT'?'selected':''; ?>>Fulltext</option> + </select> + + + </td> + <td>Sort by:</td><td> + <?php + $sort=$_GET['sort']?$_GET['sort']:'date'; + ?> + <select name="sort"> + <option value="ID" <?php echo $sort== 'ID' ?'selected':''; ?>>Ticket #</option> + <option value="pri" <?php echo $sort == 'pri' ?'selected':''; ?>>Priority</option> + <option value="date" <?php echo $sort == 'date' ?'selected':''; ?>>Date</option> + <option value="dept" <?php echo $sort == 'dept' ?'selected':''; ?>>Dept.</option> + </select> + <select name="order"> + <option value="DESC"<?php echo $_REQUEST['order'] == 'DESC' ?'selected':''; ?>>Descending</option> + <option value="ASC"<?php echo $_REQUEST['order'] == 'ASC'?'selected':''; ?>>Ascending</option> + </select> + </td> + <td>Results Per Page:</td><td> + <select name="limit"> + <?php + $sel=$_REQUEST['limit']?$_REQUEST['limit']:15; + for ($x = 5; $x <= 25; $x += 5) { ?> + <option value="<?php echo $x; ?>" <?php echo ($sel==$x )?'selected':''; ?>><?php echo $x; ?></option> + <?php } ?> + </select> + </td> + <td> + <input type="submit" name="advance_search" class="button" value="Search"> + [ <a href="#" onClick="showHide('advance','basic'); return false;" >Basic</a> ] + </td> + </tr> + </table> + </form> +</div> +<!-- SEARCH FORM END --> +<div class="clear"></div> +<div style="margin-bottom:20px"> +<form action="tickets.php" method="POST" name='tickets' onSubmit="return checkbox_checker(this,1,0);"> + <a class="refresh" href="">Refresh</a> + <input type="hidden" name="a" value="mass_process" > + <input type="hidden" name="status" value="<?php echo $status; ?>" > + <table class="list" border="0" cellspacing="1" cellpadding="2" width="940"> + <caption><?php echo $showing; ?> <?php echo $results_type; ?></caption> + <thead> + <tr> + <?php if($thisstaff->canManageTickets()) { ?> + <th width="8px"> </th> + <?php } ?> + <th width="70"> + <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Ticket ID <?php echo $negorder; ?>">Ticket</a></th> + <th width="70"> + <a <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Date <?php echo $negorder; ?>">Date</a></th> + <th width="280"> + <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Subject <?php echo $negorder; ?>">Subject</a></th> + <th width="170"> + <a <?php echo $name_sort; ?> href="tickets.php?sort=name&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Name <?php echo $negorder; ?>">From</a></th> + <?php + if($search && !$status) { ?> + <th width="60"> + <a <?php echo $status_sort; ?> href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Status <?php echo $negorder; ?>">Status</a></th> + <?php + } else { ?> + <th width="60" <?=$pri_sort?>> + <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Priority <?php echo $negorder; ?>">Priority</a></th> + <?php + } + + if($showassigned){ ?> + <th width="150"> + <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Assignee <?php echo $negorder;?>">Assigned To</a></th> + <?}elseif(!strcasecmp($status,'closed')){?> + <th width="150"> + <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Closing Staff Name <?php echo $negorder; ?>">Closed By</a></th> + <?}else{?> + <th width="150"> + <a <?=$dept_sort?> href="tickets.php?sort=dept&order=<?=$negorder?><?=$qstr?>" + title="Sort By Department <?=$negorder?>">Department</a></th> + <?}?> + </tr> + </thead> + <tbody> + <?php + $class = "row1"; + $total=0; + if($res && ($num=db_num_rows($res))): + while ($row = db_fetch_array($res)) { + $tag=$row['staff_id']?'assigned':'openticket'; + $flag=null; + if($row['lock_id']) + $flag='locked'; + elseif($row['isoverdue']) + $flag='overdue'; + + $lc=''; + if($showassigned || !strcasecmp($status,'closed')) { + if($row['staff_id']) + $lc=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate($row['staff'],40)); + elseif($row['team_id']) + $lc=sprintf('<span class="Icon teamAssigned">%s</span>',Format::truncate($row['team'],40)); + else + $lc=' '; + }else{ + $lc=Format::truncate($row['dept_name'],40); + } + $tid=$row['ticketID']; + $subject = Format::truncate($row['subject'],40); + $threadcount=$row['messages']+$row['responses']; + if(!strcasecmp($row['status'],'open') && !$row['isanswered'] && !$row['lock_id']) { + $tid=sprintf('<b>%s</b>',$tid); + } + ?> + <tr id="<?php echo $row['ticket_id']; ?>"> + <?php if($thisstaff->canManageTickets()) { ?> + <td align="center" class="nohover"> + <input type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" onClick="highLight(this.value,this.checked);"> + </td> + <?php } ?> + <td align="center" title="<?php echo $row['email']; ?>" nowrap> + <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket" + href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td> + <td align="center" nowrap><?php echo Format::db_date($row['created']); ?></td> + <td><a <?php if($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?> + href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a> + + <?php echo ($threadcount>1)?" <small>($threadcount)</small> ":''?> + <?php echo $row['attachments']?"<span class='Icon file'> </span>":''; ?> + </td> + <td nowrap> <?php echo Format::truncate($row['name'],22,strpos($row['name'],'@')); ?> </td> + <?php + if($search && !$status){ + $displaystatus=ucfirst($row['status']); + if(!strcasecmp($row['status'],'open')) + $displaystatus="<b>$displaystatus</b>"; + echo "<td>$displaystatus</td>"; + } else { ?> + <td class="nohover" align="center" style="background-color:<?php echo $row['priority_color']; ?>;"> + <?php echo $row['priority_desc']; ?></td> + <?php + } + ?> + <td nowrap> <?php echo $lc; ?></td> + </tr> + <?php + } //end of while. + else: //not tickets found!! set fetch error. + $ferror='Query returned 0 results.'; + endif; ?> + </tbody> + <tfoot> + <tr> + <td colspan="7"> + <?php if($res && $num){ ?> + Select: + <a href="#" onclick="return select_all(document.forms['tickets'],true)">All</a> + <a href="#" onclick="return reset_all(document.forms['tickets'])">None</a> + <a href="#" onclick="return toogle_all(document.forms['tickets'],true)">Toggle</a> + <?php }else{ + echo $ferror?Format::htmlchars($ferror):'Query returned 0 results.'; + } ?> + </td> + </tr> + </tfoot> + </table> + <?php + if($num>0){ //if we actually had any tickets returned. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; + ?> + <?php + if($thisstaff->canManageTickets()) { ?> + <p class="centered"> + <?php + $status=$_REQUEST['status']?$_REQUEST['status']:$status; + switch (strtolower($status)) { + case 'closed': ?> + <input class="button" type="submit" name="reopen" value="Reopen" + onClick=' return confirm("Are you sure you want to reopen selected tickets?");'> + <?php + break; + case 'open': + case 'answered': + case 'assigned': + ?> + <input class="button" type="submit" name="overdue" value="Overdue" + onClick=' return confirm("Are you sure you want to mark selected tickets overdue/stale?");'> + <input class="button" type="submit" name="close" value="Close" + onClick=' return confirm("Are you sure you want to close selected tickets?");'> + <?php + break; + default: //search?? + ?> + <input class="button" type="submit" name="close" value="Close" + onClick=' return confirm("Are you sure you want to close selected tickets?");'> + <input class="button" type="submit" name="reopen" value="Reopen" + onClick=' return confirm("Are you sure you want to reopen selected tickets?");'> + <?php + } + if($thisstaff->canDeleteTickets()) { ?> + <input class="button" type="submit" name="delete" value="Delete" + onClick=' return confirm("Are you sure you want to DELETE selected tickets?");'> + <?php } ?> + </p> + <?php + } + } ?> + </form> +</div> diff --git a/include/staff/topic.inc.php b/include/staff/topic.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..6504c5443200a4a3bd82624fec18c27c0ff3ea24 --- /dev/null +++ b/include/staff/topic.inc.php @@ -0,0 +1,80 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff->isadmin()) die('Access Denied'); + +$info=($_POST && $errors)?Format::input($_POST):array(); //Re-use the post info on error...savekeyboards.org +if($topic && $_REQUEST['a']!='new'){ + $title='Edit Topic'; + $action='update'; + $info=$info?$info:$topic->getInfo(); +}else { + $title='New Help Topic'; + $action='create'; + $info['isactive']=isset($info['isactive'])?$info['isactive']:1; +} +//get the goodies. +$depts= db_query('SELECT dept_id,dept_name FROM '.DEPT_TABLE); +$priorities= db_query('SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE); +?> +<form action="admin.php?t=topics" method="post"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type='hidden' name='t' value='topics'> + <input type="hidden" name="topic_id" value="<?php echo $info['topic_id']; ?>"> +<table width="100%" border="0" cellspacing=0 cellpadding=2 class="tform"> + <tr class="header"><td colspan=2><?php echo $title; ?></td></tr> + <tr class="subheader"> + <td colspan=2 >Disabling auto response will overwrite dept settings.</td> + </tr> + <tr> + <th width="20%">Help Topic:</th> + <td><input type="text" name="topic" size="55" value="<?php echo $info['topic']; ?>"> + <font class="error">* <?php echo $errors['topic']; ?></font></td> + </tr> + <tr><th>Topic Status</th> + <td> + <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked':''; ?> />Active + <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked':''; ?> />Disabled + </td> + </tr> + <tr> + <th nowrap>Auto Response:</th> + <td> + <input type="checkbox" name="noautoresp" value=1 <?php echo $info['noautoresp']? 'checked': ''; ?> > + <b>Disable</b> autoresponse for this topic. (<i>Overwrite Dept setting</i>) + </td> + </tr> + <tr> + <th>New Ticket Priority:</th> + <td> + <select name="priority_id"> + <option value=0>Select Priority</option> + <?php + while (list($id,$name) = db_fetch_row($priorities)){ + $selected = ($info['priority_id']==$id)?'selected':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> + <?php + } ?> + </select> <font class="error">* <?php echo $errors['priority_id']; ?></font> + </td> + </tr> + <tr> + <th nowrap>New Ticket Department:</th> + <td> + <select name="dept_id"> + <option value=0>Select Department</option> + <?php + while (list($id,$name) = db_fetch_row($depts)){ + $selected = ($info['dept_id']==$id)?'selected':''; ?> + <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?> Dept</option> + <?php + } ?> + </select> <font class="error">* <?php echo $errors['dept_id']; ?></font> + </td> + </tr> +</table> +<div style="padding-left:220px;"> + <input class="button" type="submit" name="submit" value="Submit"> + <input class="button" type="reset" name="reset" value="Reset"> + <input class="button" type="button" name="cancel" value="Cancel" onClick='window.location.href="admin.php?t=topics"'> +</div> +</form> diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..43412bcad7b0ac01aeb3ff80edc6f857232f6313 --- /dev/null +++ b/include/staff/tpl.inc.php @@ -0,0 +1,63 @@ +<?php +$msgtemplates=Template::message_templates(); +$info=Format::htmlchars(($errors && $_POST)?$_POST:$_REQUEST); +$info['tpl']=($info['tpl'] && $msgtemplates[$info['tpl']])?$info['tpl']:'ticket_autoresp'; +$tpl=$msgtemplates[$info['tpl']]; +$info=array_merge($template->getMsgTemplate($info['tpl']),$info); + +?> +<h2>Email Template Message - <span><?php echo $template->getName(); ?></span></h2> +<div style="padding-top:10px;padding-bottom:5px;"> + <form method="get" action="templates.php"> + <input type="hidden" name="id" value="<?php echo $template->getId(); ?>"> + <input type="hidden" name="a" value="manage"> + Message Template: + <select name="tpl" style="width:300px;"> + <option value="">— Select Setting Group —</option> + <?php + foreach($msgtemplates as $k=>$v) { + $sel=($info['tpl']==$k)?'selected="selected"':''; + echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v['name']); + } + ?> + </select> + <input type="submit" value="Go"> + <font color="red"><?php echo $errors['tpl']; ?></font> + </form> +</div> +<form action="templates.php?id=<?php echo $template->getId(); ?>" method="post" id="save"> +<input type="hidden" name="id" value="<?php echo $template->getId(); ?>"> +<input type="hidden" name="tpl" value="<?php echo $info['tpl']; ?>"> +<input type="hidden" name="a" value="manage"> +<input type="hidden" name="do" value="updatetpl"> + +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo Format::htmlchars($tpl['desc']); ?></h4> + <em>Subject and body required. <a class="tip" href="ticket_variables.txt">Supported Variables</a>.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td colspan=2> + <strong>Message Subject:</strong> <em>Email message subject</em> <font class="error">* <?php echo $errors['subj']; ?></font><br> + <input type="text" name="subj" size="60" value="<?php echo $info['subj']; ?>" > + </td> + </tr> + <tr> + <td colspan="2"> + <strong>Message Body:</strong> <em>Email message body.</em> <font class="error">* <?php echo $errors['body']; ?></font><br> + <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft" ><?php echo $info['body']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:210px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> + <input class="button" type="button" name="cancel" value="Cancel Changes" onclick='window.location.href="templates.php?id=<?php echo $template->getId(); ?>"'> +</p> +</form> diff --git a/index.php b/index.php new file mode 100644 index 0000000000000000000000000000000000000000..37e5c7f1b2dd3e899d3d7a154d5e074629ed1bfe --- /dev/null +++ b/index.php @@ -0,0 +1,16 @@ +<?php +/********************************************************************* + index.php + + Helpdesk landing page. Please customize it to fit your needs. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('offline.php'); diff --git a/js/jquery.min.js b/js/jquery.min.js new file mode 100644 index 0000000000000000000000000000000000000000..f78f96a12f13973ee16bc9d334e403b55502c66a --- /dev/null +++ b/js/jquery.min.js @@ -0,0 +1,16 @@ +/*! + * jQuery JavaScript Library v1.5.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Mar 31 15:28:23 2011 -0400 + */ +(function(a,b){function ci(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cf(a){if(!b_[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";b_[a]=c}return b_[a]}function ce(a,b){var c={};d.each(cd.concat.apply([],cd.slice(0,b)),function(){c[this]=a});return c}function b$(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bZ(){try{return new a.XMLHttpRequest}catch(b){}}function bY(){d(a).unload(function(){for(var a in bW)bW[a](0,1)})}function bS(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h==="string"&&(f[h.toLowerCase()]=a.converters[h]);l=k,k=e[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=f[m]||f["* "+k];if(!n){p=b;for(o in f){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=f[j[1]+" "+k];if(p){o=f[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&d.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bR(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bQ(a,b,c,e){if(d.isArray(b)&&b.length)d.each(b,function(b,f){c||bs.test(a)?e(a,f):bQ(a+"["+(typeof f==="object"||d.isArray(f)?b:"")+"]",f,c,e)});else if(c||b==null||typeof b!=="object")e(a,b);else if(d.isArray(b)||d.isEmptyObject(b))e(a,"");else for(var f in b)bQ(a+"["+f+"]",b[f],c,e)}function bP(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bJ,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l==="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bP(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bP(a,c,d,e,"*",g));return l}function bO(a){return function(b,c){typeof b!=="string"&&(c=b,b="*");if(d.isFunction(c)){var e=b.toLowerCase().split(bD),f=0,g=e.length,h,i,j;for(;f<g;f++)h=e[f],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bq(a,b,c){var e=b==="width"?bk:bl,f=b==="width"?a.offsetWidth:a.offsetHeight;if(c==="border")return f;d.each(e,function(){c||(f-=parseFloat(d.css(a,"padding"+this))||0),c==="margin"?f+=parseFloat(d.css(a,"margin"+this))||0:f-=parseFloat(d.css(a,"border"+this+"Width"))||0});return f}function bc(a,b){b.src?d.ajax({url:b.src,async:!1,dataType:"script"}):d.globalEval(b.text||b.textContent||b.innerHTML||""),b.parentNode&&b.parentNode.removeChild(b)}function bb(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function ba(a,b){if(b.nodeType===1){var c=b.nodeName.toLowerCase();b.clearAttributes(),b.mergeAttributes(a);if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(d.expando)}}function _(a,b){if(b.nodeType===1&&d.hasData(a)){var c=d.expando,e=d.data(a),f=d.data(b,e);if(e=e[c]){var g=e.events;f=f[c]=d.extend({},e);if(g){delete f.handle,f.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)d.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function $(a,b){return d.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Q(a,b,c){if(d.isFunction(b))return d.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return d.grep(a,function(a,d){return a===b===c});if(typeof b==="string"){var e=d.grep(a,function(a){return a.nodeType===1});if(L.test(b))return d.filter(b,e,!c);b=d.filter(b,e)}return d.grep(a,function(a,e){return d.inArray(a,b)>=0===c})}function P(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function H(a,b){return(a&&a!=="*"?a+".":"")+b.replace(t,"`").replace(u,"&")}function G(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,p=[],q=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;i<t.length;i++)g=t[i],g.origType.replace(r,"")===a.type?q.push(g.selector):t.splice(i--,1);f=d(a.target).closest(q,a.currentTarget);for(j=0,k=f.length;j<k;j++){m=f[j];for(i=0;i<t.length;i++){g=t[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,e=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,e=d(a.relatedTarget).closest(g.selector)[0];(!e||e!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){f=p[j];if(c&&f.level>c)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function E(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function y(){return!0}function x(){return!1}function i(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function h(a,c,e){if(e===b&&a.nodeType===1){e=a.getAttribute("data-"+c);if(typeof e==="string"){try{e=e==="true"?!0:e==="false"?!1:e==="null"?null:d.isNaN(e)?g.test(e)?d.parseJSON(e):e:parseFloat(e)}catch(f){}d.data(a,c,e)}else e=b}return e}var c=a.document,d=function(){function G(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(G,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x,y,z=Object.prototype.toString,A=Object.prototype.hasOwnProperty,B=Array.prototype.push,C=Array.prototype.slice,D=String.prototype.trim,E=Array.prototype.indexOf,F={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.2",length:0,size:function(){return this.length},toArray:function(){return C.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?B.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),x.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(C.apply(this,arguments),"slice",C.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:B,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){e=i[c],f=a[c];if(i===f)continue;l&&f&&(d.isPlainObject(f)||(g=d.isArray(f)))?(g?(g=!1,h=e&&d.isArray(e)?e:[]):h=e&&d.isPlainObject(e)?e:{},i[c]=d.extend(l,h,f)):f!==b&&(i[c]=f)}return i},d.extend({noConflict:function(b){a.$=f,b&&(a.jQuery=e);return d},isReady:!1,readyWait:1,ready:function(a){a===!0&&d.readyWait--;if(!d.readyWait||a!==!0&&!d.isReady){if(!c.body)return setTimeout(d.ready,1);d.isReady=!0;if(a!==!0&&--d.readyWait>0)return;x.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=d._Deferred();if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",y,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",y),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&G()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):F[z.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!A.call(a,"constructor")&&!A.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||A.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g<h;)if(c.apply(a[g++],e)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(var j=a[0];g<h&&c.call(j,g,j)!==!1;j=a[++g]){}return a},trim:D?function(a){return a==null?"":D.call(a)}:function(a){return a==null?"":(a+"").replace(j,"").replace(k,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var e=d.type(a);a.length==null||e==="string"||e==="function"||e==="regexp"||d.isWindow(a)?B.call(c,a):d.merge(c,a)}return c},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length==="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,b,c){var d=[],e;for(var f=0,g=a.length;f<g;f++)e=b(a[f],f,c),e!=null&&(d[d.length]=e);return d.concat.apply([],d)},guid:1,proxy:function(a,c,e){arguments.length===2&&(typeof c==="string"?(e=a,a=e[c],c=b):c&&!d.isFunction(c)&&(e=c,c=b)),!c&&a&&(c=function(){return a.apply(e||this,arguments)}),a&&(c.guid=a.guid=a.guid||c.guid||d.guid++);return c},access:function(a,c,e,f,g,h){var i=a.length;if(typeof c==="object"){for(var j in c)d.access(a,j,c[j],f,g,e);return a}if(e!==b){f=!h&&f&&d.isFunction(e);for(var k=0;k<i;k++)g(a[k],c,f?e.call(a[k],k,g(a[k],c)):e,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}d.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.subclass=this.subclass,a.fn.init=function b(b,c){c&&c instanceof d&&!(c instanceof a)&&(c=a(c));return d.fn.init.call(this,b,c,e)},a.fn.init.prototype=a.fn;var e=a(c);return a},browser:{}}),d.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){F["[object "+b+"]"]=b.toLowerCase()}),w=d.uaMatch(v),w.browser&&(d.browser[w.browser]=!0,d.browser.version=w.version),d.browser.webkit&&(d.browser.safari=!0),E&&(d.inArray=function(a,b){return E.call(b,a)}),i.test("Â ")&&(j=/^[\s\xA0]+/,k=/[\s\xA0]+$/),g=d(c),c.addEventListener?y=function(){c.removeEventListener("DOMContentLoaded",y,!1),d.ready()}:c.attachEvent&&(y=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",y),d.ready())});return d}(),e="then done fail isResolved isRejected promise".split(" "),f=[].slice;d.extend({_Deferred:function(){var a=[],b,c,e,f={done:function(){if(!e){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=d.type(i),j==="array"?f.done.apply(f,i):j==="function"&&a.push(i);k&&f.resolveWith(k[0],k[1])}return this},resolveWith:function(d,f){if(!e&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(d,f)}finally{b=[d,f],c=0}}return this},resolve:function(){f.resolveWith(this,arguments);return this},isResolved:function(){return c||b},cancel:function(){e=1,a=[];return this}};return f},Deferred:function(a){var b=d._Deferred(),c=d._Deferred(),f;d.extend(b,{then:function(a,c){b.done(a).fail(c);return this},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,promise:function(a){if(a==null){if(f)return f;f=a={}}var c=e.length;while(c--)a[e[c]]=b[e[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?f.call(arguments,0):c,--g||h.resolveWith(h,f.call(b,0))}}var b=arguments,c=0,e=b.length,g=e,h=e<=1&&a&&d.isFunction(a.promise)?a:d.Deferred();if(e>1){for(;c<e;c++)b[c]&&d.isFunction(b[c].promise)?b[c].promise().then(i(c),h.reject):--g;g||h.resolveWith(h,b)}else h!==a&&h.resolveWith(h,e?[a]:[]);return h.promise()}}),function(){d.support={};var b=c.createElement("div");b.style.display="none",b.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=b.getElementsByTagName("input")[0];if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,noCloneEvent:!0,noCloneChecked:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0,reliableMarginRight:!0},i.checked=!0,d.support.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,d.support.optDisabled=!h.disabled;var j=null;d.support.scriptEval=function(){if(j===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(j=!0,delete a[f]):j=!1,b.removeChild(e)}return j};try{delete b.test}catch(k){d.support.deleteExpando=!1}!b.addEventListener&&b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function l(){d.support.noCloneEvent=!1,b.detachEvent("onclick",l)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";var m=c.createDocumentFragment();m.appendChild(b.firstChild),d.support.checkClone=m.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(a.style.width="1px",a.style.marginRight="0",d.support.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(a,null).marginRight,10)||0)===0),b.removeChild(a).style.display="none",a=e=null}});var n=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function");return d};d.support.submitBubbles=n("submit"),d.support.changeBubbles=n("change"),b=e=f=null}}();var g=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!i(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={},j||(k[l].toJSON=d.noop));if(typeof c==="object"||typeof c==="function")f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c);i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,g=b.nodeType,h=g?d.cache:b,j=g?b[d.expando]:d.expando;if(!h[j])return;if(c){var k=e?h[j][f]:h[j];if(k){delete k[c];if(!i(k))return}}if(e){delete h[j][f];if(!i(h[j]))return}var l=h[j][f];d.support.deleteExpando||h!=a?delete h[j]:h[j]=null,l?(h[j]={},g||(h[j].toJSON=d.noop),h[j][f]=l):g&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var f=this[0].attributes,g;for(var i=0,j=f.length;i<j;i++)g=f[i].name,g.indexOf("data-")===0&&(g=g.substr(5),h(this[0],g,e[g]))}}return e}if(typeof a==="object")return this.each(function(){d.data(this,a)});var k=a.split(".");k[1]=k[1]?"."+k[1]:"";if(c===b){e=this.triggerHandler("getData"+k[1]+"!",[k[0]]),e===b&&this.length&&(e=d.data(this[0],a),e=h(this[0],a,e));return e===b&&k[1]?this.data(k[0]):e}return this.each(function(){var b=d(this),e=[k[0],c];b.triggerHandler("setData"+k[1]+"!",e),d.data(this,a,c),b.triggerHandler("changeData"+k[1]+"!",e)})},removeData:function(a){return this.each(function(){d.removeData(this,a)})}}),d.extend({queue:function(a,b,c){if(a){b=(b||"fx")+"queue";var e=d._data(a,b);if(!c)return e||[];!e||d.isArray(c)?e=d._data(a,b,d.makeArray(c)):e.push(c);return e}},dequeue:function(a,b){b=b||"fx";var c=d.queue(a,b),e=c.shift();e==="inprogress"&&(e=c.shift()),e&&(b==="fx"&&c.unshift("inprogress"),e.call(a,function(){d.dequeue(a,b)})),c.length||d.removeData(a,b+"queue",!0)}}),d.fn.extend({queue:function(a,c){typeof a!=="string"&&(c=a,a="fx");if(c===b)return d.queue(this[0],a);return this.each(function(b){var e=d.queue(this,a,c);a==="fx"&&e[0]!=="inprogress"&&d.dequeue(this,a)})},dequeue:function(a){return this.each(function(){d.dequeue(this,a)})},delay:function(a,b){a=d.fx?d.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){d.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var j=/[\n\t\r]/g,k=/\s+/,l=/\r/g,m=/^(?:href|src|style)$/,n=/^(?:button|input)$/i,o=/^(?:button|input|object|select|textarea)$/i,p=/^a(?:rea)?$/i,q=/^(?:radio|checkbox)$/i;d.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"},d.fn.extend({attr:function(a,b){return d.access(this,a,b,!0,d.attr)},removeAttr:function(a,b){return this.each(function(){d.attr(this,a,""),this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.addClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"){var b=(a||"").split(k);for(var c=0,e=this.length;c<e;c++){var f=this[c];if(f.nodeType===1)if(f.className){var g=" "+f.className+" ",h=f.className;for(var i=0,j=b.length;i<j;i++)g.indexOf(" "+b[i]+" ")<0&&(h+=" "+b[i]);f.className=d.trim(h)}else f.className=a}}return this},removeClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.removeClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"||a===b){var c=(a||"").split(k);for(var e=0,f=this.length;e<f;e++){var g=this[e];if(g.nodeType===1&&g.className)if(a){var h=(" "+g.className+" ").replace(j," ");for(var i=0,l=c.length;i<l;i++)h=h.replace(" "+c[i]+" "," ");g.className=d.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,e=typeof b==="boolean";if(d.isFunction(a))return this.each(function(c){var e=d(this);e.toggleClass(a.call(this,c,e.attr("class"),b),b)});return this.each(function(){if(c==="string"){var f,g=0,h=d(this),i=b,j=a.split(k);while(f=j[g++])i=e?i:!h.hasClass(f),h[i?"addClass":"removeClass"](f)}else if(c==="undefined"||c==="boolean")this.className&&d._data(this,"__className__",this.className),this.className=this.className||a===!1?"":d._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(j," ").indexOf(b)>-1)return!0;return!1},val:function(a){if(!arguments.length){var c=this[0];if(c){if(d.nodeName(c,"option")){var e=c.attributes.value;return!e||e.specified?c.value:c.text}if(d.nodeName(c,"select")){var f=c.selectedIndex,g=[],h=c.options,i=c.type==="select-one";if(f<0)return null;for(var j=i?f:0,k=i?f+1:h.length;j<k;j++){var m=h[j];if(m.selected&&(d.support.optDisabled?!m.disabled:m.getAttribute("disabled")===null)&&(!m.parentNode.disabled||!d.nodeName(m.parentNode,"optgroup"))){a=d(m).val();if(i)return a;g.push(a)}}if(i&&!g.length&&h.length)return d(h[f]).val();return g}if(q.test(c.type)&&!d.support.checkOn)return c.getAttribute("value")===null?"on":c.value;return(c.value||"").replace(l,"")}return b}var n=d.isFunction(a);return this.each(function(b){var c=d(this),e=a;if(this.nodeType===1){n&&(e=a.call(this,b,c.val())),e==null?e="":typeof e==="number"?e+="":d.isArray(e)&&(e=d.map(e,function(a){return a==null?"":a+""}));if(d.isArray(e)&&q.test(this.type))this.checked=d.inArray(c.val(),e)>=0;else if(d.nodeName(this,"select")){var f=d.makeArray(e);d("option",this).each(function(){this.selected=d.inArray(d(this).val(),f)>=0}),f.length||(this.selectedIndex=-1)}else this.value=e}})}}),d.extend({attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,e,f){if(!a||a.nodeType===3||a.nodeType===8||a.nodeType===2)return b;if(f&&c in d.attrFn)return d(a)[c](e);var g=a.nodeType!==1||!d.isXMLDoc(a),h=e!==b;c=g&&d.props[c]||c;if(a.nodeType===1){var i=m.test(c);if(c==="selected"&&!d.support.optSelected){var j=a.parentNode;j&&(j.selectedIndex,j.parentNode&&j.parentNode.selectedIndex)}if((c in a||a[c]!==b)&&g&&!i){h&&(c==="type"&&n.test(a.nodeName)&&a.parentNode&&d.error("type property can't be changed"),e===null?a.nodeType===1&&a.removeAttribute(c):a[c]=e);if(d.nodeName(a,"form")&&a.getAttributeNode(c))return a.getAttributeNode(c).nodeValue;if(c==="tabIndex"){var k=a.getAttributeNode("tabIndex");return k&&k.specified?k.value:o.test(a.nodeName)||p.test(a.nodeName)&&a.href?0:b}return a[c]}if(!d.support.style&&g&&c==="style"){h&&(a.style.cssText=""+e);return a.style.cssText}h&&a.setAttribute(c,""+e);if(!a.attributes[c]&&(a.hasAttribute&&!a.hasAttribute(c)))return b;var l=!d.support.hrefNormalized&&g&&i?a.getAttribute(c,2):a.getAttribute(c);return l===null?b:l}h&&(a[c]=e);return a[c]}});var r=/\.(.*)$/,s=/^(?:textarea|input|select)$/i,t=/\./g,u=/ /g,v=/[^\w\s.|`]/g,w=function(a){return a.replace(v,"\\$&")};d.event={add:function(c,e,f,g){if(c.nodeType!==3&&c.nodeType!==8){try{d.isWindow(c)&&(c!==a&&!c.frameElement)&&(c=a)}catch(h){}if(f===!1)f=x;else if(!f)return;var i,j;f.handler&&(i=f,f=i.handler),f.guid||(f.guid=d.guid++);var k=d._data(c);if(!k)return;var l=k.events,m=k.handle;l||(k.events=l={}),m||(k.handle=m=function(a){return typeof d!=="undefined"&&d.event.triggered!==a.type?d.event.handle.apply(m.elem,arguments):b}),m.elem=c,e=e.split(" ");var n,o=0,p;while(n=e[o++]){j=i?d.extend({},i):{handler:f,data:g},n.indexOf(".")>-1?(p=n.split("."),n=p.shift(),j.namespace=p.slice(0).sort().join(".")):(p=[],j.namespace=""),j.type=n,j.guid||(j.guid=f.guid);var q=l[n],r=d.event.special[n]||{};if(!q){q=l[n]=[];if(!r.setup||r.setup.call(c,g,p,m)===!1)c.addEventListener?c.addEventListener(n,m,!1):c.attachEvent&&c.attachEvent("on"+n,m)}r.add&&(r.add.call(c,j),j.handler.guid||(j.handler.guid=f.guid)),q.push(j),d.event.global[n]=!0}c=null}},global:{},remove:function(a,c,e,f){if(a.nodeType!==3&&a.nodeType!==8){e===!1&&(e=x);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=d.hasData(a)&&d._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(e=c.handler,c=c.type);if(!c||typeof c==="string"&&c.charAt(0)==="."){c=c||"";for(h in t)d.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+d.map(m.slice(0).sort(),w).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!e){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))d.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=d.event.special[h]||{};for(j=f||0;j<p.length;j++){q=p[j];if(e.guid===q.guid){if(l||n.test(q.namespace))f==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(f!=null)break}}if(p.length===0||f!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&d.removeEvent(a,h,s.handle),g=null,delete t[h]}if(d.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,d.isEmptyObject(s)&&d.removeData(a,b,!0)}}},trigger:function(a,c,e){var f=a.type||a,g=arguments[3];if(!g){a=typeof a==="object"?a[d.expando]?a:d.extend(d.Event(f),a):d.Event(f),f.indexOf("!")>=0&&(a.type=f=f.slice(0,-1),a.exclusive=!0),e||(a.stopPropagation(),d.event.global[f]&&d.each(d.cache,function(){var b=d.expando,e=this[b];e&&e.events&&e.events[f]&&d.event.trigger(a,c,e.handle.elem)}));if(!e||e.nodeType===3||e.nodeType===8)return b;a.result=b,a.target=e,c=d.makeArray(c),c.unshift(a)}a.currentTarget=e;var h=d._data(e,"handle");h&&h.apply(e,c);var i=e.parentNode||e.ownerDocument;try{e&&e.nodeName&&d.noData[e.nodeName.toLowerCase()]||e["on"+f]&&e["on"+f].apply(e,c)===!1&&(a.result=!1,a.preventDefault())}catch(j){}if(!a.isPropagationStopped()&&i)d.event.trigger(a,c,i,!0);else if(!a.isDefaultPrevented()){var k,l=a.target,m=f.replace(r,""),n=d.nodeName(l,"a")&&m==="click",o=d.event.special[m]||{};if((!o._default||o._default.call(e,a)===!1)&&!n&&!(l&&l.nodeName&&d.noData[l.nodeName.toLowerCase()])){try{l[m]&&(k=l["on"+m],k&&(l["on"+m]=null),d.event.triggered=a.type,l[m]())}catch(p){}k&&(l["on"+m]=k),d.event.triggered=b}}},handle:function(c){var e,f,g,h,i,j=[],k=d.makeArray(arguments);c=k[0]=d.event.fix(c||a.event),c.currentTarget=this,e=c.type.indexOf(".")<0&&!c.exclusive,e||(g=c.type.split("."),c.type=g.shift(),j=g.slice(0).sort(),h=new RegExp("(^|\\.)"+j.join("\\.(?:.*\\.)?")+"(\\.|$)")),c.namespace=c.namespace||j.join("."),i=d._data(this,"events"),f=(i||{})[c.type];if(i&&f){f=f.slice(0);for(var l=0,m=f.length;l<m;l++){var n=f[l];if(e||h.test(n.namespace)){c.handler=n.handler,c.data=n.data,c.handleObj=n;var o=n.handler.apply(this,k);o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[d.expando])return a;var e=a;a=d.Event(e);for(var f=this.props.length,g;f;)g=this.props[--f],a[g]=e[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=c.documentElement,i=c.body;a.pageX=a.clientX+(h&&h.scrollLeft||i&&i.scrollLeft||0)-(h&&h.clientLeft||i&&i.clientLeft||0),a.pageY=a.clientY+(h&&h.scrollTop||i&&i.scrollTop||0)-(h&&h.clientTop||i&&i.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:d.proxy,special:{ready:{setup:d.bindReady,teardown:d.noop},live:{add:function(a){d.event.add(this,H(a.origType,a.selector),d.extend({},a,{handler:G,guid:a.handler.guid}))},remove:function(a){d.event.remove(this,H(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){d.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},d.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},d.Event=function(a){if(!this.preventDefault)return new d.Event(a);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?y:x):this.type=a,this.timeStamp=d.now(),this[d.expando]=!0},d.Event.prototype={preventDefault:function(){this.isDefaultPrevented=y;var a=this.originalEvent;a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=y;var a=this.originalEvent;a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=y,this.stopPropagation()},isDefaultPrevented:x,isPropagationStopped:x,isImmediatePropagationStopped:x};var z=function(a){var b=a.relatedTarget;try{if(b&&b!==c&&!b.parentNode)return;while(b&&b!==this)b=b.parentNode;b!==this&&(a.type=a.data,d.event.handle.apply(this,arguments))}catch(e){}},A=function(a){a.type=a.data,d.event.handle.apply(this,arguments)};d.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){d.event.special[a]={setup:function(c){d.event.add(this,b,c&&c.selector?A:z,a)},teardown:function(a){d.event.remove(this,b,a&&a.selector?A:z)}}}),d.support.submitBubbles||(d.event.special.submit={setup:function(a,b){if(this.nodeName&&this.nodeName.toLowerCase()!=="form")d.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&d(b).closest("form").length&&E("submit",this,arguments)}),d.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&d(b).closest("form").length&&a.keyCode===13&&E("submit",this,arguments)});else return!1},teardown:function(a){d.event.remove(this,".specialSubmit")}});if(!d.support.changeBubbles){var B,C=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?d.map(a.options,function(a){return a.selected}).join("-"):"":a.nodeName.toLowerCase()==="select"&&(c=a.selectedIndex);return c},D=function D(a){var c=a.target,e,f;if(s.test(c.nodeName)&&!c.readOnly){e=d._data(c,"_change_data"),f=C(c),(a.type!=="focusout"||c.type!=="radio")&&d._data(c,"_change_data",f);if(e===b||f===e)return;if(e!=null||f)a.type="change",a.liveFired=b,d.event.trigger(a,arguments[1],c)}};d.event.special.change={filters:{focusout:D,beforedeactivate:D,click:function(a){var b=a.target,c=b.type;(c==="radio"||c==="checkbox"||b.nodeName.toLowerCase()==="select")&&D.call(this,a)},keydown:function(a){var b=a.target,c=b.type;(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&D.call(this,a)},beforeactivate:function(a){var b=a.target;d._data(b,"_change_data",C(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in B)d.event.add(this,c+".specialChange",B[c]);return s.test(this.nodeName)},teardown:function(a){d.event.remove(this,".specialChange");return s.test(this.nodeName)}},B=d.event.special.change.filters,B.focus=B.beforeactivate}c.addEventListener&&d.each({focus:"focusin",blur:"focusout"},function(a,b){function f(a){var c=d.event.fix(a);c.type=b,c.originalEvent={},d.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var e=0;d.event.special[b]={setup:function(){e++===0&&c.addEventListener(a,f,!0)},teardown:function(){--e===0&&c.removeEventListener(a,f,!0)}}}),d.each(["bind","one"],function(a,c){d.fn[c]=function(a,e,f){if(typeof a==="object"){for(var g in a)this[c](g,e,a[g],f);return this}if(d.isFunction(e)||e===!1)f=e,e=b;var h=c==="one"?d.proxy(f,function(a){d(this).unbind(a,h);return f.apply(this,arguments)}):f;if(a==="unload"&&c!=="one")this.one(a,e,f);else for(var i=0,j=this.length;i<j;i++)d.event.add(this[i],a,h,e);return this}}),d.fn.extend({unbind:function(a,b){if(typeof a!=="object"||a.preventDefault)for(var e=0,f=this.length;e<f;e++)d.event.remove(this[e],a,b);else for(var c in a)this.unbind(c,a[c]);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){d.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){var c=d.Event(a);c.preventDefault(),c.stopPropagation(),d.event.trigger(c,b,this[0]);return c.result}},toggle:function(a){var b=arguments,c=1;while(c<b.length)d.proxy(a,b[c++]);return this.click(d.proxy(a,function(e){var f=(d._data(this,"lastToggle"+a.guid)||0)%c;d._data(this,"lastToggle"+a.guid,f+1),e.preventDefault();return b[f].apply(this,arguments)||!1}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var F={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};d.each(["live","die"],function(a,c){d.fn[c]=function(a,e,f,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:d(this.context);if(typeof a==="object"&&!a.preventDefault){for(var o in a)n[c](o,e,a[o],m);return this}d.isFunction(e)&&(f=e,e=b),a=(a||"").split(" ");while((h=a[i++])!=null){j=r.exec(h),k="",j&&(k=j[0],h=h.replace(r,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,h==="focus"||h==="blur"?(a.push(F[h]+k),h=h+k):h=(F[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)d.event.add(n[p],"live."+H(h,m),{data:e,selector:m,handler:f,origType:h,origHandler:f,preType:l});else n.unbind("live."+H(h,m),f)}return this}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){d.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},d.attrFn&&(d.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!=="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,e,g){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!=="string")return e;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(f.call(n)==="[object Array]")if(u)if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&e.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&e.push(j[t]);else e.push.apply(e,n);else p(n,e);o&&(k(o,h,e,g),k.uniqueSort(e));return e};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!=="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(f){if(f===!0)continue}else g=o=!0}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b==="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1){}a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!=="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!=="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!=="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return"text"===c&&(b===c||b===null)},radio:function(a){return"radio"===a.type},checkbox:function(a){return"checkbox"===a.type},file:function(a){return"file"===a.type},password:function(a){return"password"===a.type},submit:function(a){return"submit"===a.type},image:function(a){return"image"===a.type},reset:function(a){return"reset"===a.type},button:function(a){return"button"===a.type||a.nodeName.toLowerCase()==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(f.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length==="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(a===b){g=!0;return 0}if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!=="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!=="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!=="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!=="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!=="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};d.find=k,d.expr=k.selectors,d.expr[":"]=d.expr.filters,d.unique=k.uniqueSort,d.text=k.getText,d.isXMLDoc=k.isXML,d.contains=k.contains}();var I=/Until$/,J=/^(?:parents|prevUntil|prevAll)/,K=/,/,L=/^.[^:#\[\.,]*$/,M=Array.prototype.slice,N=d.expr.match.POS,O={children:!0,contents:!0,next:!0,prev:!0};d.fn.extend({find:function(a){var b=this.pushStack("","find",a),c=0;for(var e=0,f=this.length;e<f;e++){c=b.length,d.find(a,this[e],b);if(e>0)for(var g=c;g<b.length;g++)for(var h=0;h<c;h++)if(b[h]===b[g]){b.splice(g--,1);break}}return b},has:function(a){var b=d(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(d.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(Q(this,a,!1),"not",a)},filter:function(a){return this.pushStack(Q(this,a,!0),"filter",a)},is:function(a){return!!a&&d.filter(a,this).length>0},closest:function(a,b){var c=[],e,f,g=this[0];if(d.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(e=0,f=a.length;e<f;e++)i=a[e],j[i]||(j[i]=d.expr.match.POS.test(i)?d(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:d(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=N.test(a)?d(a,b||this.context):null;for(e=0,f=this.length;e<f;e++){g=this[e];while(g){if(l?l.index(g)>-1:d.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b)break}}c=c.length>1?d.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a==="string")return d.inArray(this[0],a?d(a):this.parent().children());return d.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a==="string"?d(a,b):d.makeArray(a),e=d.merge(this.get(),c);return this.pushStack(P(c[0])||P(e[0])?e:d.unique(e))},andSelf:function(){return this.add(this.prevObject)}}),d.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return d.dir(a,"parentNode")},parentsUntil:function(a,b,c){return d.dir(a,"parentNode",c)},next:function(a){return d.nth(a,2,"nextSibling")},prev:function(a){return d.nth(a,2,"previousSibling")},nextAll:function(a){return d.dir(a,"nextSibling")},prevAll:function(a){return d.dir(a,"previousSibling")},nextUntil:function(a,b,c){return d.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return d.dir(a,"previousSibling",c)},siblings:function(a){return d.sibling(a.parentNode.firstChild,a)},children:function(a){return d.sibling(a.firstChild)},contents:function(a){return d.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:d.makeArray(a.childNodes)}},function(a,b){d.fn[a]=function(c,e){var f=d.map(this,b,c),g=M.call(arguments);I.test(a)||(e=c),e&&typeof e==="string"&&(f=d.filter(e,f)),f=this.length>1&&!O[a]?d.unique(f):f,(this.length>1||K.test(e))&&J.test(a)&&(f=f.reverse());return this.pushStack(f,a,g.join(","))}}),d.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?d.find.matchesSelector(b[0],a)?[b[0]]:[]:d.find.matches(a,b)},dir:function(a,c,e){var f=[],g=a[c];while(g&&g.nodeType!==9&&(e===b||g.nodeType!==1||!d(g).is(e)))g.nodeType===1&&f.push(g),g=g[c];return f},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var R=/ jQuery\d+="(?:\d+|null)"/g,S=/^\s+/,T=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,U=/<([\w:]+)/,V=/<tbody/i,W=/<|&#?\w+;/,X=/<(?:script|object|embed|option|style)/i,Y=/checked\s*(?:[^=]|=\s*.checked.)/i,Z={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};Z.optgroup=Z.option,Z.tbody=Z.tfoot=Z.colgroup=Z.caption=Z.thead,Z.th=Z.td,d.support.htmlSerialize||(Z._default=[1,"div<div>","</div>"]),d.fn.extend({text:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.text(a.call(this,b,c.text()))});if(typeof a!=="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return d.text(this)},wrapAll:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapAll(a.call(this,b))});if(this[0]){var b=d(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapInner(a.call(this,b))});return this.each(function(){var b=d(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){d(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=d(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,d(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,e;(e=this[c])!=null;c++)if(!a||d.filter(a,[e]).length)!b&&e.nodeType===1&&(d.cleanData(e.getElementsByTagName("*")),d.cleanData([e])),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&d.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return d.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(R,""):null;if(typeof a!=="string"||X.test(a)||!d.support.leadingWhitespace&&S.test(a)||Z[(U.exec(a)||["",""])[1].toLowerCase()])d.isFunction(a)?this.each(function(b){var c=d(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);else{a=a.replace(T,"<$1></$2>");try{for(var c=0,e=this.length;c<e;c++)this[c].nodeType===1&&(d.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(f){this.empty().append(a)}}return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(d.isFunction(a))return this.each(function(b){var c=d(this),e=c.html();c.replaceWith(a.call(this,b,e))});typeof a!=="string"&&(a=d(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;d(this).remove(),b?d(b).before(a):d(c).append(a)})}return this.length?this.pushStack(d(d.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,e){var f,g,h,i,j=a[0],k=[];if(!d.support.checkClone&&arguments.length===3&&typeof j==="string"&&Y.test(j))return this.each(function(){d(this).domManip(a,c,e,!0)});if(d.isFunction(j))return this.each(function(f){var g=d(this);a[0]=j.call(this,f,c?g.html():b),g.domManip(a,c,e)});if(this[0]){i=j&&j.parentNode,d.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?f={fragment:i}:f=d.buildFragment(a,this,k),h=f.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&d.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)e.call(c?$(this[l],g):this[l],f.cacheable||m>1&&l<n?d.clone(h,!0,!0):h)}k.length&&d.each(k,bc)}return this}}),d.buildFragment=function(a,b,e){var f,g,h,i=b&&b[0]?b[0].ownerDocument||b[0]:c;a.length===1&&typeof a[0]==="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!X.test(a[0])&&(d.support.checkClone||!Y.test(a[0]))&&(g=!0,h=d.fragments[a[0]],h&&(h!==1&&(f=h))),f||(f=i.createDocumentFragment(),d.clean(a,i,f,e)),g&&(d.fragments[a[0]]=h?f:1);return{fragment:f,cacheable:g}},d.fragments={},d.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){d.fn[a]=function(c){var e=[],f=d(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&f.length===1){f[b](this[0]);return this}for(var h=0,i=f.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();d(f[h])[b](j),e=e.concat(j)}return this.pushStack(e,a,f.selector)}}),d.extend({clone:function(a,b,c){var e=a.cloneNode(!0),f,g,h;if((!d.support.noCloneEvent||!d.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!d.isXMLDoc(a)){ba(a,e),f=bb(a),g=bb(e);for(h=0;f[h];++h)ba(f[h],g[h])}if(b){_(a,e);if(c){f=bb(a),g=bb(e);for(h=0;f[h];++h)_(f[h],g[h])}}return e},clean:function(a,b,e,f){b=b||c,typeof b.createElement==="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var g=[];for(var h=0,i;(i=a[h])!=null;h++){typeof i==="number"&&(i+="");if(!i)continue;if(typeof i!=="string"||W.test(i)){if(typeof i==="string"){i=i.replace(T,"<$1></$2>");var j=(U.exec(i)||["",""])[1].toLowerCase(),k=Z[j]||Z._default,l=k[0],m=b.createElement("div");m.innerHTML=k[1]+i+k[2];while(l--)m=m.lastChild;if(!d.support.tbody){var n=V.test(i),o=j==="table"&&!n?m.firstChild&&m.firstChild.childNodes:k[1]==="<table>"&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&S.test(i)&&m.insertBefore(b.createTextNode(S.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var bd=/alpha\([^)]*\)/i,be=/opacity=([^)]*)/,bf=/-([a-z])/ig,bg=/([A-Z]|^ms)/g,bh=/^-?\d+(?:px)?$/i,bi=/^-?\d/,bj={position:"absolute",visibility:"hidden",display:"block"},bk=["Left","Right"],bl=["Top","Bottom"],bm,bn,bo,bp=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bm(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bm)return bm(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bf,bp)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bq(a,b,e):d.swap(a,bj,function(){f=bq(a,b,e)});if(f<=0){f=bm(a,b,b),f==="0px"&&bo&&(f=bo(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!bh.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return be.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=bd.test(f)?f.replace(bd,e):c.filter+" "+e}}),d(function(){d.support.reliableMarginRight||(d.cssHooks.marginRight={get:function(a,b){var c;d.swap(a,{display:"inline-block"},function(){b?c=bm(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bn=function(a,c,e){var f,g,h;e=e.replace(bg,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bo=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bh.test(d)&&bi.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bm=bn||bo,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var br=/%20/g,bs=/\[\]$/,bt=/\r?\n/g,bu=/#.*$/,bv=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bw=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bx=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,by=/^(?:GET|HEAD)$/,bz=/^\/\//,bA=/\?/,bB=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bC=/^(?:select|textarea)/i,bD=/\s+/,bE=/([?&])_=[^&]*/,bF=/(^|\-)([a-z])/g,bG=function(a,b,c){return b+c.toUpperCase()},bH=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bI=d.fn.load,bJ={},bK={},bL,bM;try{bL=c.location.href}catch(bN){bL=c.createElement("a"),bL.href="",bL=bL.href}bM=bH.exec(bL.toLowerCase())||[],d.fn.extend({load:function(a,c,e){if(typeof a!=="string"&&bI)return bI.apply(this,arguments);if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var g=a.slice(f,a.length);a=a.slice(0,f)}var h="GET";c&&(d.isFunction(c)?(e=c,c=b):typeof c==="object"&&(c=d.param(c,d.ajaxSettings.traditional),h="POST"));var i=this;d.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?d("<div>").append(c.replace(bB,"")).find(g):c)),e&&i.each(e,[c,b,a])}});return this},serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?d.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bC.test(this.nodeName)||bw.test(this.type))}).map(function(a,b){var c=d(this).val();return c==null?null:d.isArray(c)?d.map(c,function(a,c){return{name:b.name,value:a.replace(bt,"\r\n")}}):{name:b.name,value:c.replace(bt,"\r\n")}}).get()}}),d.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){d.fn[b]=function(a){return this.bind(b,a)}}),d.each(["get","post"],function(a,c){d[c]=function(a,e,f,g){d.isFunction(e)&&(g=g||f,f=e,e=b);return d.ajax({type:c,url:a,data:e,success:f,dataType:g})}}),d.extend({getScript:function(a,c){return d.get(a,b,c,"script")},getJSON:function(a,b,c){return d.get(a,b,c,"json")},ajaxSetup:function(a,b){b?d.extend(!0,a,d.ajaxSettings,b):(b=a,a=d.extend(!0,d.ajaxSettings,b));for(var c in {context:1,url:1})c in b?a[c]=b[c]:c in d.ajaxSettings&&(a[c]=d.ajaxSettings[c]);return a},ajaxSettings:{url:bL,isLocal:bx.test(bM[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML}},ajaxPrefilter:bO(bJ),ajaxTransport:bO(bK),ajax:function(a,c){function v(a,c,l,n){if(r!==2){r=2,p&&clearTimeout(p),o=b,m=n||"",u.readyState=a?4:0;var q,t,v,w=l?bR(e,u,l):b,x,y;if(a>=200&&a<300||a===304){if(e.ifModified){if(x=u.getResponseHeader("Last-Modified"))d.lastModified[k]=x;if(y=u.getResponseHeader("Etag"))d.etag[k]=y}if(a===304)c="notmodified",q=!0;else try{t=bS(e,w),c="success",q=!0}catch(z){c="parsererror",v=z}}else{v=c;if(!c||a)c="error",a<0&&(a=0)}u.status=a,u.statusText=c,q?h.resolveWith(f,[t,c,u]):h.rejectWith(f,[u,c,v]),u.statusCode(j),j=b,s&&g.trigger("ajax"+(q?"Success":"Error"),[u,e,q?t:v]),i.resolveWith(f,[u,c]),s&&(g.trigger("ajaxComplete",[u,e]),--d.active||d.event.trigger("ajaxStop"))}}typeof a==="object"&&(c=a,a=b),c=c||{};var e=d.ajaxSetup({},c),f=e.context||e,g=f!==e&&(f.nodeType||f instanceof d)?d(f):d.event,h=d.Deferred(),i=d._Deferred(),j=e.statusCode||{},k,l={},m,n,o,p,q,r=0,s,t,u={readyState:0,setRequestHeader:function(a,b){r||(l[a.toLowerCase().replace(bF,bG)]=b);return this},getAllResponseHeaders:function(){return r===2?m:null},getResponseHeader:function(a){var c;if(r===2){if(!n){n={};while(c=bv.exec(m))n[c[1].toLowerCase()]=c[2]}c=n[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){r||(e.mimeType=a);return this},abort:function(a){a=a||"abort",o&&o.abort(a),v(0,a);return this}};h.promise(u),u.success=u.done,u.error=u.fail,u.complete=i.done,u.statusCode=function(a){if(a){var b;if(r<2)for(b in a)j[b]=[j[b],a[b]];else b=a[u.status],u.then(b,b)}return this},e.url=((a||e.url)+"").replace(bu,"").replace(bz,bM[1]+"//"),e.dataTypes=d.trim(e.dataType||"*").toLowerCase().split(bD),e.crossDomain==null&&(q=bH.exec(e.url.toLowerCase()),e.crossDomain=q&&(q[1]!=bM[1]||q[2]!=bM[2]||(q[3]||(q[1]==="http:"?80:443))!=(bM[3]||(bM[1]==="http:"?80:443)))),e.data&&e.processData&&typeof e.data!=="string"&&(e.data=d.param(e.data,e.traditional)),bP(bJ,e,c,u);if(r===2)return!1;s=e.global,e.type=e.type.toUpperCase(),e.hasContent=!by.test(e.type),s&&d.active++===0&&d.event.trigger("ajaxStart");if(!e.hasContent){e.data&&(e.url+=(bA.test(e.url)?"&":"?")+e.data),k=e.url;if(e.cache===!1){var w=d.now(),x=e.url.replace(bE,"$1_="+w);e.url=x+(x===e.url?(bA.test(e.url)?"&":"?")+"_="+w:"")}}if(e.data&&e.hasContent&&e.contentType!==!1||c.contentType)l["Content-Type"]=e.contentType;e.ifModified&&(k=k||e.url,d.lastModified[k]&&(l["If-Modified-Since"]=d.lastModified[k]),d.etag[k]&&(l["If-None-Match"]=d.etag[k])),l.Accept=e.dataTypes[0]&&e.accepts[e.dataTypes[0]]?e.accepts[e.dataTypes[0]]+(e.dataTypes[0]!=="*"?", */*; q=0.01":""):e.accepts["*"];for(t in e.headers)u.setRequestHeader(t,e.headers[t]);if(e.beforeSend&&(e.beforeSend.call(f,u,e)===!1||r===2)){u.abort();return!1}for(t in {success:1,error:1,complete:1})u[t](e[t]);o=bP(bK,e,c,u);if(o){u.readyState=1,s&&g.trigger("ajaxSend",[u,e]),e.async&&e.timeout>0&&(p=setTimeout(function(){u.abort("timeout")},e.timeout));try{r=1,o.send(l,v)}catch(y){status<2?v(-1,y):d.error(y)}}else v(-1,"No Transport");return u},param:function(a,c){var e=[],f=function(a,b){b=d.isFunction(b)?b():b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=d.ajaxSettings.traditional);if(d.isArray(a)||a.jquery&&!d.isPlainObject(a))d.each(a,function(){f(this.name,this.value)});else for(var g in a)bQ(g,a[g],c,f);return e.join("&").replace(br,"+")}}),d.extend({active:0,lastModified:{},etag:{}});var bT=d.now(),bU=/(\=)\?(&|$)|\?\?/i;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return d.expando+"_"+bT++}}),d.ajaxPrefilter("json jsonp",function(b,c,e){var f=typeof b.data==="string";if(b.dataTypes[0]==="jsonp"||c.jsonpCallback||c.jsonp!=null||b.jsonp!==!1&&(bU.test(b.url)||f&&bU.test(b.data))){var g,h=b.jsonpCallback=d.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2",m=function(){a[h]=i,g&&d.isFunction(i)&&a[h](g[0])};b.jsonp!==!1&&(j=j.replace(bU,l),b.url===j&&(f&&(k=k.replace(bU,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},e.then(m,m),b.converters["script json"]=function(){g||d.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){d.globalEval(a);return a}}}),d.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),d.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var bV=d.now(),bW,bX;d.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&bZ()||b$()}:bZ,bX=d.ajaxSettings.xhr(),d.support.ajax=!!bX,d.support.cors=bX&&"withCredentials"in bX,bX=b,d.support.ajax&&d.ajaxTransport(function(a){if(!a.crossDomain||d.support.cors){var c;return{send:function(e,f){var g=a.xhr(),h,i;a.username?g.open(a.type,a.url,a.async,a.username,a.password):g.open(a.type,a.url,a.async);if(a.xhrFields)for(i in a.xhrFields)g[i]=a.xhrFields[i];a.mimeType&&g.overrideMimeType&&g.overrideMimeType(a.mimeType),!a.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(i in e)g.setRequestHeader(i,e[i])}catch(j){}g.send(a.hasContent&&a.data||null),c=function(e,i){var j,k,l,m,n;try{if(c&&(i||g.readyState===4)){c=b,h&&(g.onreadystatechange=d.noop,delete bW[h]);if(i)g.readyState!==4&&g.abort();else{j=g.status,l=g.getAllResponseHeaders(),m={},n=g.responseXML,n&&n.documentElement&&(m.xml=n),m.text=g.responseText;try{k=g.statusText}catch(o){k=""}j||!a.isLocal||a.crossDomain?j===1223&&(j=204):j=m.text?200:404}}}catch(p){i||f(-1,p)}m&&f(j,k,m,l)},a.async&&g.readyState!==4?(bW||(bW={},bY()),h=bV++,g.onreadystatechange=bW[h]=c):c()},abort:function(){c&&c(0,1)}}}});var b_={},ca=/^(?:toggle|show|hide)$/,cb=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cc,cd=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];d.fn.extend({show:function(a,b,c){var e,f;if(a||a===0)return this.animate(ce("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)e=this[g],f=e.style.display,!d._data(e,"olddisplay")&&f==="none"&&(f=e.style.display=""),f===""&&d.css(e,"display")==="none"&&d._data(e,"olddisplay",cf(e.nodeName));for(g=0;g<h;g++){e=this[g],f=e.style.display;if(f===""||f==="none")e.style.display=d._data(e,"olddisplay")||""}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ce("hide",3),a,b,c);for(var e=0,f=this.length;e<f;e++){var g=d.css(this[e],"display");g!=="none"&&!d._data(this[e],"olddisplay")&&d._data(this[e],"olddisplay",g)}for(e=0;e<f;e++)this[e].style.display="none";return this},_toggle:d.fn.toggle,toggle:function(a,b,c){var e=typeof a==="boolean";d.isFunction(a)&&d.isFunction(b)?this._toggle.apply(this,arguments):a==null||e?this.each(function(){var b=e?a:d(this).is(":hidden");d(this)[b?"show":"hide"]()}):this.animate(ce("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,e){var f=d.speed(b,c,e);if(d.isEmptyObject(a))return this.each(f.complete);return this[f.queue===!1?"each":"queue"](function(){var b=d.extend({},f),c,e=this.nodeType===1,g=e&&d(this).is(":hidden"),h=this;for(c in a){var i=d.camelCase(c);c!==i&&(a[i]=a[c],delete a[c],c=i);if(a[c]==="hide"&&g||a[c]==="show"&&!g)return b.complete.call(this);if(e&&(c==="height"||c==="width")){b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY];if(d.css(this,"display")==="inline"&&d.css(this,"float")==="none")if(d.support.inlineBlockNeedsLayout){var j=cf(this.nodeName);j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)}else this.style.display="inline-block"}d.isArray(a[c])&&((b.specialEasing=b.specialEasing||{})[c]=a[c][1],a[c]=a[c][0])}b.overflow!=null&&(this.style.overflow="hidden"),b.curAnim=d.extend({},a),d.each(a,function(c,e){var f=new d.fx(h,b,c);if(ca.test(e))f[e==="toggle"?g?"show":"hide":e](a);else{var i=cb.exec(e),j=f.cur();if(i){var k=parseFloat(i[2]),l=i[3]||(d.cssNumber[c]?"":"px");l!=="px"&&(d.style(h,c,(k||1)+l),j=(k||1)/f.cur()*j,d.style(h,c,j+l)),i[1]&&(k=(i[1]==="-="?-1:1)*k+j),f.custom(j,k,l)}else f.custom(j,e,"")}});return!0})},stop:function(a,b){var c=d.timers;a&&this.queue([]),this.each(function(){for(var a=c.length-1;a>=0;a--)c[a].elem===this&&(b&&c[a](!0),c.splice(a,1))}),b||this.dequeue();return this}}),d.each({slideDown:ce("show",1),slideUp:ce("hide",1),slideToggle:ce("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){d.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),d.extend({speed:function(a,b,c){var e=a&&typeof a==="object"?d.extend({},a):{complete:c||!c&&b||d.isFunction(a)&&a,duration:a,easing:c&&b||b&&!d.isFunction(b)&&b};e.duration=d.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in d.fx.speeds?d.fx.speeds[e.duration]:d.fx.speeds._default,e.old=e.complete,e.complete=function(){e.queue!==!1&&d(this).dequeue(),d.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig||(b.orig={})}}),d.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(d.fx.step[this.prop]||d.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=d.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function g(a){return e.step(a)}var e=this,f=d.fx;this.startTime=d.now(),this.start=a,this.end=b,this.unit=c||this.unit||(d.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,g.elem=this.elem,g()&&d.timers.push(g)&&!cc&&(cc=setInterval(f.tick,f.interval))},show:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),d(this.elem).show()},hide:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=d.now(),c=!0;if(a||b>=this.options.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),this.options.curAnim[this.prop]=!0;for(var e in this.options.curAnim)this.options.curAnim[e]!==!0&&(c=!1);if(c){if(this.options.overflow!=null&&!d.support.shrinkWrapBlocks){var f=this.elem,g=this.options;d.each(["","X","Y"],function(a,b){f.style["overflow"+b]=g.overflow[a]})}this.options.hide&&d(this.elem).hide();if(this.options.hide||this.options.show)for(var h in this.options.curAnim)d.style(this.elem,h,this.options.orig[h]);this.options.complete.call(this.elem)}return!1}var i=b-this.startTime;this.state=i/this.options.duration;var j=this.options.specialEasing&&this.options.specialEasing[this.prop],k=this.options.easing||(d.easing.swing?"swing":"linear");this.pos=d.easing[j||k](this.state,i,0,1,this.options.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update();return!0}},d.extend(d.fx,{tick:function(){var a=d.timers;for(var b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||d.fx.stop()},interval:13,stop:function(){clearInterval(cc),cc=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){d.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),d.expr&&d.expr.filters&&(d.expr.filters.animated=function(a){return d.grep(d.timers,function(b){return a===b.elem}).length});var cg=/^t(?:able|d|h)$/i,ch=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?d.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(e){}var f=b.ownerDocument,g=f.documentElement;if(!c||!d.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=f.body,i=ci(f),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||d.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||d.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:d.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);d.offset.initialize();var c,e=b.offsetParent,f=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(d.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===e&&(l+=b.offsetTop,m+=b.offsetLeft,d.offset.doesNotAddBorder&&(!d.offset.doesAddBorderForTableAndCells||!cg.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),f=e,e=b.offsetParent),d.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;d.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},d.offset={initialize:function(){var a=c.body,b=c.createElement("div"),e,f,g,h,i=parseFloat(d.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";d.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),e=b.firstChild,f=e.firstChild,h=e.nextSibling.firstChild.firstChild,this.doesNotAddBorder=f.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,f.style.position="fixed",f.style.top="20px",this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15,f.style.position=f.style.top="",e.style.overflow="hidden",e.style.position="relative",this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),d.offset.initialize=d.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;d.offset.initialize(),d.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(d.css(a,"marginTop"))||0,c+=parseFloat(d.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var e=d.css(a,"position");e==="static"&&(a.style.position="relative");var f=d(a),g=f.offset(),h=d.css(a,"top"),i=d.css(a,"left"),j=(e==="absolute"||e==="fixed")&&d.inArray("auto",[h,i])>-1,k={},l={},m,n;j&&(l=f.position()),m=j?l.top:parseInt(h,10)||0,n=j?l.left:parseInt(i,10)||0,d.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):f.css(k)}},d.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),e=ch.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(d.css(a,"marginTop"))||0,c.left-=parseFloat(d.css(a,"marginLeft"))||0,e.top+=parseFloat(d.css(b[0],"borderTopWidth"))||0,e.left+=parseFloat(d.css(b[0],"borderLeftWidth"))||0;return{top:c.top-e.top,left:c.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&(!ch.test(a.nodeName)&&d.css(a,"position")==="static"))a=a.offsetParent;return a})}}),d.each(["Left","Top"],function(a,c){var e="scroll"+c;d.fn[e]=function(c){var f=this[0],g;if(!f)return null;if(c!==b)return this.each(function(){g=ci(this),g?g.scrollTo(a?d(g).scrollLeft():c,a?c:d(g).scrollTop()):this[e]=c});g=ci(f);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:d.support.boxModel&&g.document.documentElement[e]||g.document.body[e]:f[e]}}),d.each(["Height","Width"],function(a,c){var e=c.toLowerCase();d.fn["inner"+c]=function(){return this[0]?parseFloat(d.css(this[0],e,"padding")):null},d.fn["outer"+c]=function(a){return this[0]?parseFloat(d.css(this[0],e,a?"margin":"border")):null},d.fn[e]=function(a){var f=this[0];if(!f)return a==null?null:this;if(d.isFunction(a))return this.each(function(b){var c=d(this);c[e](a.call(this,b,c[e]()))});if(d.isWindow(f)){var g=f.document.documentElement["client"+c];return f.document.compatMode==="CSS1Compat"&&g||f.document.body["client"+c]||g}if(f.nodeType===9)return Math.max(f.documentElement["client"+c],f.body["scroll"+c],f.documentElement["scroll"+c],f.body["offset"+c],f.documentElement["offset"+c]);if(a===b){var h=d.css(f,e),i=parseFloat(h);return d.isNaN(i)?h:i}return this.css(e,typeof a==="string"?a:a+"px")}}),a.jQuery=a.$=d})(window); \ No newline at end of file diff --git a/js/osticket.js b/js/osticket.js new file mode 100644 index 0000000000000000000000000000000000000000..fd04ff48c809525267ebaacca85ac30cb37dd00a --- /dev/null +++ b/js/osticket.js @@ -0,0 +1,79 @@ +jQuery(function($) { + var max_uploads = 5; + var current_reply_uploads = 0; + var current_note_uploads = 0; + + function parse_upload(elem) { + var new_input = elem.clone(); + var filename = elem.val(); + if(filename != '') { + var container = elem.parent().parent(); + var form_type = container.attr('id'); + elem.blur().hide(); + $('.uploads', container).append('<div><label><input type="checkbox" name="uploads[]" value="' + filename + '" checked="checked"> ' + filename.replace('C:\\', '').replace('fakepath\\', '') + '</label></div>'); + if(form_type=='reply_form_attachments') { + current_reply_uploads++; + if(current_reply_uploads < max_uploads) { + elem.after(new_input.val('').blur()); + } + } else { + current_note_uploads++; + if(current_note_uploads < max_uploads) { + elem.after(new_input.val('').blur()); + } + } + } + } + + if($.browser.msie) { + $('.attachments').delegate('input[type=file]', 'click', function() { + var elem = $(this); + setTimeout(function() { + parse_upload(elem); + elem.blur(); + }, 0); + }); + } else { + $('.attachments').delegate('input[type=file]', 'change', function() { + var elem = $(this); + parse_upload(elem); + }); + } + + $('.uploads').delegate('.uploads input', 'click', function(e) { + e.preventDefault(); + var elem = $(this); + elem.attr('checked', 'checked'); + if(confirm("Are you sure you want to delete this attachment?")==true) { + var container = elem.parent().parent(); + var cparent = container.parent().parent(); + var form_type = cparent.attr('id'); + var filename = elem.val(); + $('input[type=file]', cparent).each(function() { + if($(this).val() == filename) { + $(this).remove(); + } + }); + container.remove(); + var new_input = $('input[type=file]:last', cparent).clone(); + var last_elem = $('input[type=file]:last', cparent); + if(form_type=='reply_form_attachments') { + current_reply_uploads--; + if(current_reply_uploads < max_uploads) { + if(last_elem.css('display')=='none') { + last_elem.after(new_input.val('').show()); + } + } + } else { + current_note_uploads--; + if(current_note_uploads < max_uploads) { + if(last_elem.css('display')=='none') { + last_elem.after(new_input.val('').show()); + } + } + } + } else { + e.preventDefault(); + } + }); +}); \ No newline at end of file diff --git a/kb/faq.php b/kb/faq.php new file mode 100644 index 0000000000000000000000000000000000000000..e0c1661c4ff62f8bc85b6c492b2eaf4467d19570 --- /dev/null +++ b/kb/faq.php @@ -0,0 +1,36 @@ +<?php +/********************************************************************* + faq.php + + FAQs Clients' interface.. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('kb.inc.php'); +require_once(INCLUDE_DIR.'class.faq.php'); + +$faq=$category=null; +if($_REQUEST['id'] && !($faq=FAQ::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid FAQ'; + +if(!$faq && $_REQUEST['cid'] && !($category=Category::lookup($_REQUEST['cid']))) + $errors['err']='Unknown or invalid FAQ category'; + + +$inc='knowledgebase.inc.php'; //FAQs landing page. +if($faq && $faq->isPublished()) { + $inc='faq.inc.php'; +} elseif($category && $category->isPublic() && $_REQUEST['a']!='search') { + $inc='kb-category.inc.php'; +} +require_once(CLIENTINC_DIR.'header.inc.php'); +require_once(CLIENTINC_DIR.$inc); +require_once(CLIENTINC_DIR.'footer.inc.php'); +?> diff --git a/kb/file.php b/kb/file.php new file mode 100644 index 0000000000000000000000000000000000000000..80d881707b8206860ca6461c39b4e2ad2c792c1b --- /dev/null +++ b/kb/file.php @@ -0,0 +1,30 @@ +<?php +/********************************************************************* + file.php + + Simply downloads the file...on hash validation as follows; + + * Hash must be 64 chars long. + * First 32 chars is the perm. file hash + * Next 32 chars is md5(file_id.session_id().file_hash) + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('kb.inc.php'); +require_once(INCLUDE_DIR.'class.file.php'); +$h=trim($_GET['h']); +//basic checks +if(!$h || strlen($h)!=64 //32*2 + || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. + || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getHash()))) //next 32 is file id + session hash. + die('Unknown or invalid file. #'.Format::htmlchars($_GET['h'])); + +$file->download(); +?> diff --git a/kb/index.php b/kb/index.php new file mode 100644 index 0000000000000000000000000000000000000000..1963d391627032ad4e51d2ce604035cc288cc87f --- /dev/null +++ b/kb/index.php @@ -0,0 +1,22 @@ +<?php +/********************************************************************* + index.php + + Knowledgebase Index. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('kb.inc.php'); +require_once(INCLUDE_DIR.'class.category.php'); +$inc='knowledgebase.inc.php'; +require(CLIENTINC_DIR.'header.inc.php'); +require(CLIENTINC_DIR.$inc); +require(CLIENTINC_DIR.'footer.inc.php'); +?> diff --git a/kb/kb.inc.php b/kb/kb.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..9811e63c509038ad4dcb865f1b0ad11171df619f --- /dev/null +++ b/kb/kb.inc.php @@ -0,0 +1,26 @@ +<?php +/********************************************************************* + kb.inc.php + + File included on every knowledgebase file. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +define('ROOT_PATH','../'); +require_once('../client.inc.php'); +require_once(INCLUDE_DIR.'class.faq.php'); +/* Bail out if knowledgebase is disabled or if we have no public-published FAQs. */ +if(!$cfg || !$cfg->isKnowledgebaseEnabled() || !FAQ::countPublishedFAQs()) { + header('Location: ../'); + exit; +} + +$nav = new UserNav($thisclient, 'kb'); +?> diff --git a/main.inc.php b/main.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..6fd211ed3b4505f825dc8869e92e8186d92c1e68 --- /dev/null +++ b/main.inc.php @@ -0,0 +1,190 @@ +<?php +/********************************************************************* + main.inc.php + + Master include file which must be included at the start of every file. + The brain of the whole sytem. Don't monkey with it. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + + #Disable direct access. + if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); + + #Disable Globals if enabled....before loading config info + if(ini_get('register_globals')) { + ini_set('register_globals',0); + foreach($_REQUEST as $key=>$val) + if(isset($$key)) + unset($$key); + } + + #Disable url fopen && url include + ini_set('allow_url_fopen', 0); + ini_set('allow_url_include', 0); + + #Disable session ids on url. + ini_set('session.use_trans_sid', 0); + #No cache + ini_set('session.cache_limiter', 'nocache'); + #Cookies + //ini_set('session.cookie_path','/osticket/'); + + #Error reporting...Good idea to ENABLE error reporting to a file. i.e display_errors should be set to false + error_reporting(E_ALL ^ E_NOTICE); //Respect whatever is set in php.ini (sysadmin knows better??) + #Don't display errors + ini_set('display_errors',1); + ini_set('display_startup_errors',1); + + #Set Dir constants + if(!defined('ROOT_PATH')) define('ROOT_PATH','./'); //root path. Damn directories + + define('ROOT_DIR',str_replace('\\\\', '/', realpath(dirname(__FILE__))).'/'); #Get real path for root dir ---linux and windows + define('INCLUDE_DIR',ROOT_DIR.'include/'); //Change this if include is moved outside the web path. + define('PEAR_DIR',INCLUDE_DIR.'pear/'); + define('SETUP_DIR',INCLUDE_DIR.'setup/'); + + /*############## Do NOT monkey with anything else beyond this point UNLESS you really know what you are doing ##############*/ + + #Current version && schema signature (Changes from version to version) + define('THIS_VERSION','1.7 DPR'); //Shown on admin panel + define('SCHEMA_SIGNATURE','ssddsdsd'); //MD5 signature of the db schema. (used to trigger upgrades) + + #load config info + $configfile=''; + if(file_exists(ROOT_DIR.'ostconfig.php')) //Old installs prior to v 1.6 RC5 + $configfile=ROOT_DIR.'ostconfig.php'; + elseif(file_exists(INCLUDE_DIR.'settings.php')) //OLD config file.. v 1.6 RC5 + $configfile=INCLUDE_DIR.'settings.php'; + elseif(file_exists(INCLUDE_DIR.'ost-config.php')) //NEW config file v 1.6 stable ++ + $configfile=INCLUDE_DIR.'ost-config.php'; + elseif(file_exists(ROOT_DIR.'include/')) + header('Location: '.ROOT_PATH.'setup/'); + + if(!$configfile || !file_exists($configfile)) die('<b>Error loading settings. Contact admin.</b>'); + + require($configfile); + define('CONFIG_FILE',$configfile); //used in admin.php to check perm. + + //Path separator + if(!defined('PATH_SEPARATOR')){ + if(strpos($_ENV['OS'],'Win')!==false || !strcasecmp(substr(PHP_OS, 0, 3),'WIN')) + define('PATH_SEPARATOR', ';' ); //Windows + else + define('PATH_SEPARATOR',':'); //Linux + } + + //Set include paths. Overwrite the default paths. + ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR); + + + #include required files + require(INCLUDE_DIR.'class.ostsession.php'); + require(INCLUDE_DIR.'class.usersession.php'); + require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper! + require(INCLUDE_DIR.'class.sys.php'); //system loader & config & logger. + require(INCLUDE_DIR.'class.log.php'); + require(INCLUDE_DIR.'class.mcrypt.php'); + require(INCLUDE_DIR.'class.misc.php'); + require(INCLUDE_DIR.'class.http.php'); + require(INCLUDE_DIR.'class.nav.php'); + require(INCLUDE_DIR.'class.format.php'); //format helpers + require(INCLUDE_DIR.'class.validator.php'); //Class to help with basic form input validation...please help improve it. + require(INCLUDE_DIR.'mysql.php'); + + #CURRENT EXECUTING SCRIPT. + define('THISPAGE',Misc::currentURL()); + + #pagenation default + define('PAGE_LIMIT',20); + + # This is to support old installations. with no secret salt. + if(!defined('SECRET_SALT')) define('SECRET_SALT',md5(TABLE_PREFIX.ADMIN_EMAIL)); + + #Session related + define('SESSION_SECRET', MD5(SECRET_SALT)); //Not that useful anymore... + define('SESSION_TTL', 86400); // Default 24 hours + + define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5); + define('DEFAULT_PRIORITY_ID',1); + + define('EXT_TICKET_ID_LEN',6); //Ticket create. when you start getting collisions. Applies only on random ticket ids. + + #Tables being used sytem wide + define('CONFIG_TABLE',TABLE_PREFIX.'config'); + define('SYSLOG_TABLE',TABLE_PREFIX.'syslog'); + define('SESSION_TABLE',TABLE_PREFIX.'session'); + define('FILE_TABLE',TABLE_PREFIX.'file'); + + define('STAFF_TABLE',TABLE_PREFIX.'staff'); + define('DEPT_TABLE',TABLE_PREFIX.'department'); + define('TOPIC_TABLE',TABLE_PREFIX.'help_topic'); + define('GROUP_TABLE',TABLE_PREFIX.'groups'); + define('TEAM_TABLE',TABLE_PREFIX.'team'); + define('TEAM_MEMBER_TABLE',TABLE_PREFIX.'team_member'); + + define('FAQ_TABLE',TABLE_PREFIX.'faq'); + define('FAQ_ATTACHMENT_TABLE',TABLE_PREFIX.'faq_attachment'); + define('FAQ_TOPIC_TABLE',TABLE_PREFIX.'faq_topic'); + define('FAQ_CATEGORY_TABLE',TABLE_PREFIX.'faq_category'); + define('CANNED_TABLE',TABLE_PREFIX.'canned_response'); + define('CANNED_ATTACHMENT_TABLE',TABLE_PREFIX.'canned_attachment'); + + define('TICKET_TABLE',TABLE_PREFIX.'ticket'); + define('TICKET_NOTE_TABLE',TABLE_PREFIX.'ticket_note'); + define('TICKET_MESSAGE_TABLE',TABLE_PREFIX.'ticket_message'); + define('TICKET_RESPONSE_TABLE',TABLE_PREFIX.'ticket_response'); + define('TICKET_ATTACHMENT_TABLE',TABLE_PREFIX.'ticket_attachment'); + define('TICKET_PRIORITY_TABLE',TABLE_PREFIX.'ticket_priority'); + define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE); + define('TICKET_LOCK_TABLE',TABLE_PREFIX.'ticket_lock'); + define('TICKET_HISTORY_TABLE',TABLE_PREFIX.'ticket_history'); + + define('EMAIL_TABLE',TABLE_PREFIX.'email'); + define('EMAIL_TEMPLATE_TABLE',TABLE_PREFIX.'email_template'); + define('EMAIL_FILTER_TABLE',TABLE_PREFIX.'email_filter'); + define('EMAIL_FILTER_RULE_TABLE',TABLE_PREFIX.'email_filter_rule'); + define('BANLIST_TABLE',TABLE_PREFIX.'email_banlist'); //Not in use anymore....as of v 1.7 + + define('SLA_TABLE',TABLE_PREFIX.'sla'); + + define('API_KEY_TABLE',TABLE_PREFIX.'api_key'); + define('TIMEZONE_TABLE',TABLE_PREFIX.'timezone'); + + #Connect to the DB && get configuration from database + $ferror=null; + if (!db_connect(DBHOST,DBUSER,DBPASS) || !db_select_database(DBNAME)) { + $ferror='Unable to connect to the database'; + }elseif(!($cfg=Sys::getConfig())){ + $ferror='Unable to load config info from DB. Get tech support.'; + }elseif(!ini_get('short_open_tag')) { + $ferror='Short open tag disabled! - osTicket requires it turned ON.'; + } + + if($ferror){ //Fatal error + Sys::alertAdmin('osTicket Fatal Error',$ferror); //try alerting admin. + die("<b>Fatal Error:</b> Contact system administrator."); //Generic error. + exit; + } + //Init + $cfg->init(); + //Start session handler! + $session=osTicketSession::start(SESSION_TTL); // start_session + //Set default timezone...staff will overwrite it. + $_SESSION['TZ_OFFSET']=$cfg->getTZoffset(); + $_SESSION['daylight']=$cfg->observeDaylightSaving(); + + #Cleanup magic quotes crap. + if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { + $_POST=Format::strip_slashes($_POST); + $_GET=Format::strip_slashes($_GET); + $_REQUEST=Format::strip_slashes($_REQUEST); + } +?> diff --git a/offline.php b/offline.php new file mode 100644 index 0000000000000000000000000000000000000000..3c3b24a425c78badc335c45cc62a9022fa61d1f6 --- /dev/null +++ b/offline.php @@ -0,0 +1,25 @@ +<?php +/********************************************************************* + offline.php + + Offline page...modify to fit your needs. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once('client.inc.php'); +$nav=null; +require(CLIENTINC_DIR.'header.inc.php'); +?> +<div id="landing_page"> + <h1>Support Ticket System Offline</h1> + <p>Thank you for your interest in contacting us.</p> + <p>Our helpdesk is offline at the moment, please check back at a later time.</p> +</div> +<?php require(CLIENTINC_DIR.'footer.inc.php'); ?> diff --git a/scp/admin.inc.php b/scp/admin.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..a580fa6986b87661b039419bcf178e5ae46ad12e --- /dev/null +++ b/scp/admin.inc.php @@ -0,0 +1,55 @@ +<?php +/********************************************************************* + admin.inc.php + + Handles all admin related pages....everything admin! + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +//Make sure config is loaded and the staff is set and of admin type +if(!$cfg or !$thisstaff or !$thisstaff->isadmin()){ + header('Location: index.php'); + require('index.php'); // just in case! + exit; +} + +//Some security related warnings - bitch until fixed!!! :) +if(defined('THIS_VERSION') && strcasecmp($cfg->getVersion(),THIS_VERSION)) { + $sysnotice=sprintf('The script is version %s while the database is version %s.',THIS_VERSION,$cfg->getVersion()); + if(file_exists('../setup/')) + $sysnotice.=' Possibly caused by incomplete <a href="../setup/upgrade.php">upgrade</a>.'; + $errors['err']=$sysnotice; +}elseif(!$cfg->isHelpDeskOffline()) { + if(file_exists('../setup/')){ + $sysnotice='Please take a minute to delete <strong>setup/install</strong> directory for security reasons.'; + }else{ + + if(CONFIG_FILE && file_exists(CONFIG_FILE) && is_writable(CONFIG_FILE)) { + //Confirm for real that the file is writable by group or world. + clearstatcache(); //clear the cache! + $perms = @fileperms(CONFIG_FILE); + if(($perms & 0x0002) || ($perms & 0x0010)) { + $sysnotice=sprintf('Please change permission of config file (%s) to remove write access. e.g <i>chmod 644 %s</i>', + basename(CONFIG_FILE),basename(CONFIG_FILE)); + } + } + + } + if(!$sysnotice && ini_get('register_globals')) + $sysnotice='Please consider turning off register globals if possible'; +} + +//Define some constants. +define('OSTADMININC',TRUE); //checked by admin include files +define('ADMINPAGE',TRUE); //Used by the header to swap menus. +//Admin navigation - overwrites what was set in staff.inc.php +$nav = new AdminNav($thisstaff); +?> diff --git a/scp/admin.php b/scp/admin.php new file mode 100644 index 0000000000000000000000000000000000000000..d84edfed805878660e72fa9712270168bc6a6107 --- /dev/null +++ b/scp/admin.php @@ -0,0 +1,4 @@ +<?php +header('Location: syslogs.php'); +require('syslogs.php'); +?> diff --git a/scp/ajax.php b/scp/ajax.php new file mode 100644 index 0000000000000000000000000000000000000000..a65de32d2d166af48661eec772cedc414d3b87a1 --- /dev/null +++ b/scp/ajax.php @@ -0,0 +1,59 @@ +<?php +/********************************************************************* + ajax.php + + Ajax utils interface. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +# Override staffLoginPage() defined in staff.inc.php to return an +# HTTP/Forbidden status rather than the actual login page. +# XXX: This should be moved to the AjaxController class +function staffLoginPage($msg='Unauthorized') { + Http::response(403,'Must login: '.Format::htmlchars($msg)); + exit; +} + +require('staff.inc.php'); + +//Clean house...don't let the world see your crap. +ini_set('display_errors','0'); //Disable error display +ini_set('display_startup_errors','0'); + +//TODO: disable direct access via the browser? i,e All request must have REFER? +if(!defined('INCLUDE_DIR')) Http::response(500,'config error'); + +require_once INCLUDE_DIR."/class.dispatcher.php"; +require_once INCLUDE_DIR."/class.ajax.php"; +$dispatcher = patterns("", + url("^/kb/", patterns("ajax.kbase.php:KbaseAjaxAPI", + # Send ticket-id as a query arg => canned-response/33?ticket=83 + url_get("^canned-response/(?P<id>\d+).(?P<format>json|txt)", "cannedResp"), + url_get("^faq/(?P<id>\d+)","faq") + )), + url("^/content/", patterns("ajax.content.php:ContentAjaxAPI", + url_get("^log/(?P<id>\d+)", 'log'), + url_get("^ticket_variables",'ticket_variables') + )), + url("^/config/", patterns("ajax.config.php:ConfigAjaxAPI", + url_get("^ui",'ui') + )), + url_get("^/ticket$", array("ajax.tickets.php:TicketsAjaxAPI", "search")), + url("^/ticket/", patterns("ajax.tickets.php:TicketsAjaxAPI", + url_get("^(?P<tid>\d+)/preview", "previewTicket"), + url_get("^(?P<tid>\d+)/lock", "acquireLock"), + url_post("^(?P<tid>\d+)/lock/(?P<id>\d+)/renew", "renewLock"), + url_post("^(?P<tid>\d+)/lock/(?P<id>\d+)/release", "releaseLock") + )) +); + +# Call the respective function +print $dispatcher->resolve($_SERVER['PATH_INFO']); +?> diff --git a/scp/apikeys.php b/scp/apikeys.php new file mode 100644 index 0000000000000000000000000000000000000000..ece24444537c8243a91779e59704314a672efe65 --- /dev/null +++ b/scp/apikeys.php @@ -0,0 +1,101 @@ +<?php +/********************************************************************* + apikeys.php + + API keys. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.api.php'); + +$api=null; +if($_REQUEST['id'] && !($api=API::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid API key ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$api){ + $errors['err']='Unknown or invalid API key.'; + }elseif($api->update($_POST,$errors)){ + $msg='API key updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating API key. Try again!'; + } + break; + case 'add': + if(($id=API::add($_POST,$errors))){ + $msg='API key added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add an API key. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one API key'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.API_KEY_TABLE.' SET isactive=1 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected API keys enabled'; + else + $warn="$num of $count selected API keys enabled"; + }else{ + $errors['err']='Unable to enable selected API keys.'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.API_KEY_TABLE.' SET isactive=0 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected API keys disabled'; + else + $warn="$num of $count selected API keys disabled"; + }else{ + $errors['err']='Unable to disable selected API keys'; + } + + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=API::lookup($v)) && $t->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected API keys deleted successfully'; + elseif($i>0) + $warn="$i of $count selected API keys deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected API keys'; + + }else { + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='apikeys.inc.php'; +if($api || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='apikey.inc.php'; + +$nav->setTabActive('settings'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/attachment.php b/scp/attachment.php new file mode 100644 index 0000000000000000000000000000000000000000..4b0b2c31d15aac736d9f63c46f60c209bbf026cd --- /dev/null +++ b/scp/attachment.php @@ -0,0 +1,31 @@ +<?php +/********************************************************************* + attachment.php + + Handles attachment downloads & access validation. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.attachment.php'); + +//Basic checks +if(!$thisstaff || !$_GET['id'] || !$_GET['h'] + || !($attachment=Attachment::lookup($_GET['id'])) + || !($file=$attachment->getFile())) + die('Unknown attachment!'); + +//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! +$vhash=md5($attachment->getFileId().session_id().$file->getHash()); +if(strcasecmp(trim($_GET['h']),$vhash) || !($ticket=$attachment->getTicket()) || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied'); + +//Download the file.. +$file->download(); +?> diff --git a/scp/autocron.php b/scp/autocron.php new file mode 100644 index 0000000000000000000000000000000000000000..589ea416e85d0ef5524d1bd8d9f28ce590bce0ef --- /dev/null +++ b/scp/autocron.php @@ -0,0 +1,44 @@ +<?php +/********************************************************************* + cron.php + + Auto-cron handle. + File requested as 1X1 image on the footer of every staff's page + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +ignore_user_abort(1);//Leave me a lone bro! +@set_time_limit(0); //useless when safe_mode is on +$data=sprintf ("%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%", + 71,73,70,56,57,97,1,0,1,0,128,255,0,192,192,192,0,0,0,33,249,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59); +$datasize=strlen($data); +header('Content-type: image/gif'); +header('Cache-Control: no-cache, must-revalidate'); +header("Content-Length: $datasize"); +header('Connection: Close'); +print $data; + +ob_start(); //Keep the image output clean. Hide our dirt. +//TODO: Make cron DB based to allow for better time limits. Direct calls for now sucks big time. +//We DON'T want to spawn cron on every page load...we record the lastcroncall on the session per user +$sec=time()-$_SESSION['lastcroncall']; +if($sec>180): //user can call cron once every 3 minutes. +require_once(INCLUDE_DIR.'class.cron.php'); +Cron::TicketMonitor(); //Age tickets: We're going to age tickets ever regardless of cron settings. +if($cfg && $cfg->enableAutoCron()){ //ONLY fetch tickets if autocron is enabled! + Cron::MailFetcher(); //Fetch mail. + Sys::log(LOG_DEBUG,'Autocron','cron job executed ['.$thisstaff->getUserName().']'); +} +$_SESSION['lastcroncall']=time(); +endif; +$output = ob_get_contents(); +ob_end_clean(); +?> diff --git a/scp/banlist.php b/scp/banlist.php new file mode 100644 index 0000000000000000000000000000000000000000..1ef580a5746e7666c7aebf4f72e364acb30456ea --- /dev/null +++ b/scp/banlist.php @@ -0,0 +1,124 @@ +<?php +/********************************************************************* + banlist.php + + List of banned email addresses + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.banlist.php'); + +/* Get the system ban list filter */ +if(!($filter=Banlist::getFilter())) + $warn='System ban list is empty.'; +elseif(!$filter->isActive()) + $warn='SYSTEM BAN LIST filter is <b>DISABLED</b> - <a href="filters.php">enable here</a>.'; + +$rule=null; //ban rule obj. +if($filter && $_REQUEST['id'] && !($rule=$filter->getRule($_REQUEST['id']))) + $errors['err']='Unknown or invalid ban list ID #'; + +if($_POST && !$errors && $filter){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$rule){ + $errors['err']='Unknown or invalid ban rule.'; + }elseif(!$_POST['val'] || !Validator::is_email($_POST['val'])){ + $errors['err']=$errors['val']='Valid email address required'; + }elseif(!$errors){ + $vars=array('w'=>'email', + 'h'=>'equal', + 'v'=>$_POST['val'], + 'filter_id'=>$filter->getId(), + 'isactive'=>$_POST['isactive'], + 'notes'=>$_POST['notes']); + if($rule->update($vars,$errors)){ + $msg='Email updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating ban rule. Try again!'; + } + } + break; + case 'add': + if(!$filter) { + $errors['err']='Unknown or invalid ban list'; + }elseif(!$_POST['val'] || !Validator::is_email($_POST['val'])) { + $errors['err']=$errors['val']='Valid email address required'; + }elseif(BanList::includes($_POST['val'])) { + $errors['err']=$errors['val']='Email already in the ban list'; + }elseif($filter->addRule('email','equal',$_POST['val'],array('isactive'=>$_POST['isactive'],'notes'=>$_POST['notes']))) { + $msg='Email address added to ban list successfully'; + $_REQUEST['a']=null; + //Add filter rule here. + }elseif(!$errors['err']){ + $errors['err']='Error creating ban rule. Try again!'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one email to process.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.EMAIL_FILTER_RULE_TABLE.' SET isactive=1 WHERE filter_id='.db_input($filter->getID()). + ' AND id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected emails ban status set to enabled'; + else + $warn="$num of $count selected emails enabled"; + }else{ + $errors['err']='Unable to enable selected emails'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.EMAIL_FILTER_RULE_TABLE.' SET isactive=0 WHERE filter_id='.db_input($filter->getID()). + ' AND id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected emails ban status set to disabled'; + else + $warn="$num of $count selected emails ban status set to disabled"; + }else{ + $errors['err']='Unable to disable selected emails'; + } + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($r=FilterRule::lookup($v)) && $r->delete()) + $i++; + } + if($i && $i==$count) + $msg='Selected emailes deleted successfully'; + elseif($i>0) + $warn="$i of $count selected emails deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected emails'; + + }else{ + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='banlist.inc.php'; +if(!$filter || ($rule || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add')))) + $page='banrule.inc.php'; + +$nav->setTabActive('emails'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/canned.php b/scp/canned.php new file mode 100644 index 0000000000000000000000000000000000000000..1cb77bc0884c026fd3a0c6626a74b8f21fea1f2e --- /dev/null +++ b/scp/canned.php @@ -0,0 +1,126 @@ +<?php +/********************************************************************* + canned.php + + Canned Replies aka Premade Responses. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +include_once(INCLUDE_DIR.'class.canned.php'); +/* check permission */ +if(!$thisstaff || !$thisstaff->canManageCannedResponses()) { + header('Location: kb.php'); + exit; +} + +//TODO: Support attachments! + +$canned=null; +if($_REQUEST['id'] && !($canned=Canned::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid canned reply ID.'; + +if($_POST && $thisstaff->canManageCannedResponses()) { + switch(strtolower($_POST['do'])) { + case 'update': + if(!$canned) { + $errors['err']='Unknown or invalid canned reply.'; + } elseif($canned->update($_POST, $errors)) { + $msg='Canned reply updated successfully'; + //Delete removed attachments. + //XXX: files[] shouldn't be changed under any circumstances. + $keepers = $_POST['files']?$_POST['files']:array(); + $attachments = $canned->getAttachments(); //current list of attachments. + foreach($attachments as $k=>$file) { + if($file['id'] && !in_array($file['id'], $keepers)) { + $canned->deleteAttachment($file['id']); + } + } + //Upload NEW attachments IF ANY - TODO: validate attachment types?? + if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + $canned->uploadAttachments($files); + + $canned->reload(); + + } elseif(!$errors['err']) { + $errors['err']='Error updating canned reply. Try again!'; + } + break; + case 'create': + if(($id=Canned::create($_POST, $_FILES['attachments'], $errors))) { + $msg='Canned response added successfully'; + $_REQUEST['a']=null; + //Upload attachments + if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=Format::files($_FILES['attachments']))) + $c->uploadAttachments($files); + + } elseif(!$errors['err']) { + $errors['err']='Unable to add canned response. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one canned response'; + } else { + $count=count($_POST['ids']); + if($_POST['enable']) { + $sql='UPDATE '.CANNED_TABLE.' SET isenabled=1 WHERE canned_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected canned replies enabled'; + else + $warn="$num of $count selected canned replies enabled"; + } else { + $errors['err']='Unable to enable selected canned replies.'; + } + } elseif($_POST['disable']) { + $sql='UPDATE '.CANNED_TABLE.' SET isenabled=0 WHERE canned_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected canned replies disabled'; + else + $warn="$num of $count selected canned replies disabled"; + } else { + $errors['err']='Unable to disable selected canned replies'; + } + }elseif($_POST['delete']) { + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($c=Canned::lookup($v)) && $c->delete()) + $i++; + } + + if($i==$count) + $msg='Selected canned replies deleted successfully'; + elseif($i>0) + $warn="$i of $count selected canned replies deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected canned replies'; + + } else { + $errors['err']='Unknown command'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='cannedreplies.inc.php'; +if($canned || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='cannedreply.inc.php'; + +$nav->setTabActive('kbase'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/categories.php b/scp/categories.php new file mode 100644 index 0000000000000000000000000000000000000000..787b81b5f2bb50d4aa63cdb7a15b8f3adf52af27 --- /dev/null +++ b/scp/categories.php @@ -0,0 +1,107 @@ +<?php +/********************************************************************* + categories.php + + FAQ categories + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +include_once(INCLUDE_DIR.'class.category.php'); + +/* check permission */ +if(!$thisstaff || !$thisstaff->canManageFAQ()) { + header('Location: kb.php'); + exit; +} + + +$category=null; +if($_REQUEST['id'] && !($category=Category::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid category ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])) { + case 'update': + if(!$category) { + $errors['err']='Unknown or invalid category.'; + } elseif($category->update($_POST,$errors)) { + $msg='Category updated successfully'; + } elseif(!$errors['err']) { + $errors['err']='Error updating category. Try again!'; + } + break; + case 'create': + if(($id=Category::create($_POST,$errors))) { + $msg='Category added successfully'; + $_REQUEST['a']=null; + } elseif(!$errors['err']) { + $errors['err']='Unable to add category. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one category'; + } else { + $count=count($_POST['ids']); + if($_POST['public']) { + $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=1 WHERE category_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected categories made PUBLIC'; + else + $warn="$num of $count selected categories made PUBLIC"; + } else { + $errors['err']='Unable to enable selected categories public.'; + } + } elseif($_POST['private']) { + $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=0 WHERE category_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected categories made PRIVATE'; + else + $warn="$num of $count selected categories made PRIVATE"; + } else { + $errors['err']='Unable to disable selected categories PRIVATE'; + } + }elseif($_POST['delete']) { + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($c=Category::lookup($v)) && $c->delete()) + $i++; + } + + if($i==$count) + $msg='Selected categories deleted successfully'; + elseif($i>0) + $warn="$i of $count selected categories deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected categories'; + + } else { + $errors['err']='Unknown command'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='categories.inc.php'; +if($category || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='category.inc.php'; + +$nav->setTabActive('kbase'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/css/login.css b/scp/css/login.css new file mode 100644 index 0000000000000000000000000000000000000000..8516ea0e06cdf7f6a755a1cf31504da350f1df36 --- /dev/null +++ b/scp/css/login.css @@ -0,0 +1,88 @@ +body { + background:url(/images/bg.gif) #fff; + font-family:arial, helvetica, sans-serif; + font-size:10pt; + color:#000; +} +body#loginBody { + text-align: center; + margin: 100px; +} +h1#logo { + float: none; + width: 190px; + height: 60px; + padding:10px 0 20px 0; + background: url(../images/logo-support.gif) center center no-repeat #fff; + margin: 0 auto 0 auto; +} +h1#logo a, h1#logo a:link, h1#logo a:visited, h1#logo a:hover { + display: block; + width: 190px; + height: 60px; + text-indent: -999em; + text-decoration: none; + background: none; + margin:0 auto 0 auto; +} + +input:focus, textarea:focus { + color: #F70; + background: #FEA; + -moz-outline-style: none; +} + +input[type="hidden"] { border: 0; } + +.submit { + font-family: Arial, Helvetica, sans-serif; + margin:10px auto 10px auto; + text-shadow: #333 -1px -1px 0px; + background-color: #DB8606; + color: #FFF; + border:1px solid #666; + font-weight:bold; + width:auto; +} + +input[type="submit"]:focus { + border: 1px solid #E50; + border-right-color: #FBA; + border-bottom-color: #FBA; + background: #F70; + color: #FEA; +} + +h1 { font-size: 0.9em; color: #F70; margin: 0; text-align: center;} + + +div#loginBox { + width: 300px; + padding: 10px 20px 5px 20px; + margin: 0 auto 0 auto; + background: #fff; + text-align: center; + border:5px solid #ddd; +} +div#copyRights { + font-size: 0.8em; + text-align: center; + color:#666; +} + +#copyRights a, #copyRights a:link, #copyRights a:visited, #copyRights a:hover { + + text-decoration: none; + background: none; + color:#666; +} + + +input { + width: 175px; + margin-left: 5px +} +input[type="submit"] { width: auto; margin:10px auto 10px auto; } + + +table,form { margin-top:2px; padding: 0; } diff --git a/scp/css/scp.css b/scp/css/scp.css new file mode 100644 index 0000000000000000000000000000000000000000..85a4712cdca6d4b928694df2555d125831d9a385 --- /dev/null +++ b/scp/css/scp.css @@ -0,0 +1,1051 @@ +body { + background:#eee; + font-family:arial, helvetica, sans-serif; + font-size:10pt; + color:#000; + margin:0; + padding:0; +} + +a { + color:#E65524; + text-decoration:none; +} + +.centered { + text-align:center; +} + +.clear { + clear:both; +} + +.faded { + color:#666; +} + +.strike { text-decoration:line-through; color:red; } + +#canned_attachments label { padding:3px; padding-right:10px; } + + +#breadcrumbs { + color: #333; + margin-bottom: 15px; +} + +#breadcrumbs a { + color: #555; +} + +#msg_notice { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png?1300763726') 10px 50% no-repeat #e0ffe0; } + +#msg_warning { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #f26522; background: url('../images/icons/alert.png?1307823786') 10px 50% no-repeat #ffffdd; } + +#msg_error { margin: 0; padding: 5px 10px 5px 36px; height: 16px; line-height: 16px; margin-bottom: 10px; border: 1px solid #a00; background: url('../images/icons/error.png') 10px 50% no-repeat #fff0f0; } + + +#container { + width:960px; + margin:0 auto 20px auto; +} + +#header { + height:76px; + background:url(../images/header-bg.png) top left repeat-x; + border-left:1px solid #aaa; + border-right:1px solid #aaa; +} + +#logo { + display:block; + float:left; + width:190px; + height:76px; + text-decoration:none; + outline:none; + text-indent:-9999px; + background:url(../images/ost-logo.png) top left no-repeat; +} + +#header p { + display:block; + width:430px; + float:right; + margin:10px; + background:#eee; + border:1px solid #ccc; + padding:8px; + text-align:center; +} + +#nav, #sub_nav { + clear:both; + margin:0; + padding:0 20px; + height:26px; + line-height:26px; + border-left:1px solid #aaa; + border-right:1px solid #aaa; +} + +#nav .active, #sub_nav li { + margin:0; + padding:0; + list-style:none; + display:inline; +} + +#nav { + background:#eee; + padding-top:4px; + z-index:200; + border-top:1px solid #ddd; + border-bottom:1px solid #c5d9ec; +} + +#nav .active a, #nav .inactive { + display:block; + float:left; + width:115px; + height:26px; + color:#555; + text-align:center; + font-weight:bold; + margin-top:1px; + margin-right:5px; + position:relative; +} + +#nav .inactive a { + color:#555; + display:block; +} + +#nav .active a { + background:url(../images/tab-bg.png) top left no-repeat; + color:#004a80; +} + +#nav .inactive ul { + display:none; + width:230px; + background:#fbfbfb; + margin:0; + padding:0; + position:relative; + z-index:500; + border-bottom:1px solid #ccc; + border-left:1px solid #ccc; + border-right:1px solid #ccc; +} + +#nav .inactive li { + display:block; + margin:0; + padding:0 5px; + list-style:none; + text-align:left; +} + +#nav .inactive:hover { + background:url(../images/tab-bg.png) bottom left no-repeat; +} + +#nav .inactive:hover ul { + display:block; + -moz-box-shadow: 3px 3px 3px #ccc; + -webkit-box-shadow: 3px 3px 3px #ccc; + box-shadow: 3px 3px 3px #ccc; +} + +.ieshadow { + width:230px; + background:#000; + filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius=3,MakeShadow=true,ShadowOpacity=0.30); + -ms-filter: "progid:DXImageTransform.Microsoft.Blur(PixelRadius=3,MakeShadow=true,ShadowOpacity=0.30)"; + zoom: 1; + z-index:300; + position:absolute; + top:24px; + left:0; +} + +#nav .inactive li { + background:#fbfbfb; +} + +#nav .inactive li a { + padding-left:24px; + background-position:0 50%; + background-repeat:no-repeat; + font-weight:normal; + background-color:#fbfbfb; +} + +#nav .inactive li a:hover { + color:#E65524; +} + +#sub_nav { + background:#f7f7f7; + border-bottom:1px solid #bebebe; +} + +#sub_nav a { + display:block; + float:left; + margin-right:10px; + padding:0 10px 0 21px; + background-position:0 50%; + background-repeat:no-repeat; + color:#000; +} + +#sub_nav a:hover { + color:#E65524; +} + +#sub_nav a.active { + font-weight:bold; +} + +#sub_nav .open { background-image:url(../images/icons/open.gif) } +#sub_nav .answered { background-image:url(../images/icons/answered.gif) } +#sub_nav .mine { background-image:url(../images/icons/mine.gif) } +#sub_nav .closed { background-image:url(../images/icons/closed.gif) } +#sub_nav .new { background-image:url(../images/icons/new.gif) } + +a.test { background-image:url(../images/icons/open.gif) } + +a.Ticket { background:url(../images/icons/open_tickets.gif) } +a.assignedTickets { background:url(../images/icons/assigned_tickets.gif) } +a.overdueTickets { background:url(../images/icons/overdue_tickets.gif) } +a.answeredTickets { background:url(../images/icons/answered_tickets.gif) } +a.closedTickets { background:url(../images/icons/closed_tickets.gif) } +a.newTicket { background:url(../images/icons/new_ticket.gif) } + +a.premade { background:url(../images/icons/premade_reply.gif) } +a.newPremade { background:url(../images/icons/new_premade_reply.gif) } + +a.staff { background:url(../images/icons/list_groups.gif) } +a.user { background:url(../images/icons/list_users.gif) } +a.userPref { background:url(../images/icons/user_preferences.gif) } +a.userPasswd { background:url(../images/icons/change_password.gif) } + +a.preferences { background:url(../images/icons/settings.gif) } +a.attachment { background:url(../images/icons/attachment.gif ) } +a.api { background:url(../images/icons/api.png) } +a.newapi { background:url(../images/icons/new_api.png) } + +a.sla { background:url(../images/icons/slas.png) } +a.newsla { background:url(../images/icons/new_sla.png) } + +a.logs { background:url(../images/icons/logs.gif) } + +a.emails { background:url(../images/icons/emails.png) } +a.newEmail { background:url(../images/icons/new_email.png) } + +a.emailTemplates { background:url(../images/icons/email_templates.png) } +a.newEmailTemplate { background:url(../images/icons/new_email_template.png) } + +a.emailFilters { background:url(../images/icons/email_filters.png) } +a.newEmailFilter { background:url(../images/icons/new_email_filter.png) } + +a.emailSettings { background:url(../images/icons/emails.png) } +a.emailDiagnostic { background:url(../images/icons/email_diagnostic.gif) } +a.banList { background:url(../images/icons/ban_list.gif) } + +a.users { background:url(../images/icons/list_users.gif) } +a.newuser { background:url(../images/icons/new_user.gif) } +a.groups { background:url(../images/icons/list_groups.gif) } +a.teams { background:url(../images/icons/teams.gif) } +a.newgroup { background:url(../images/icons/new_group.gif) } + +a.helpTopics { background:url(../images/icons/help_topics.png) } +a.newHelpTopic { background:url(../images/icons/new_help_topic.png) } + +a.departments { background:url(../images/icons/list_departments.gif) } +a.newDepartment { background:url(../images/icons/new_department.gif) } + + +/* Generic CSS based Icons. use=> <tag class="Icon iconname">text</tag> */ + +.Icon { + width: auto; + padding-left:20px; + background-position: left center; + background-repeat: no-repeat; +} + + +a.Icon { background-repeat: no-repeat;} + + +a.Icon:hover { + text-decoration: underline; +} + + +.Icon.newstaff { background:url(../images/icons/new_user.gif) 0 0 no-repeat; } +.Icon.newteam { background:url(../images/icons/new_team.gif) 0 0 no-repeat; } + +.Icon.Ticket { background:url(../images/icons/ticket.gif) 0 2px no-repeat; } +.Icon.webTicket { background:url(../images/icons/ticket_source_web.gif) 0 0 no-repeat; } +.Icon.emailTicket { background:url(../images/icons/ticket_source_email.gif) 0 0 no-repeat; } +.Icon.phoneTicket { background:url(../images/icons/ticket_source_phone.gif) 0 0 no-repeat; } +.Icon.otherTicket { background:url(../images/icons/ticket_source_other.gif) 0 0 no-repeat; } +.Icon.overdueTicket { background:url(../images/icons/overdue_ticket.gif) 0 0 no-repeat; } +.Icon.assignedTicket { background:url(../images/icons/assigned_ticket.gif) 0 0 no-repeat; } +.Icon.lockedTicket { background:url(../images/icons/locked_ticket.gif) 0 0 no-repeat; } +.Icon.editTicket { background-image: url(../images/icons/edit_ticket.png); } + +.Icon.file { background-image: url(../images/icons/file.gif); } +.Icon.refresh { background-image: url(../images/icons/refresh.gif); } +.Icon.note { + font-weight: bold; + font-size: 1em; + background-image: url(../images/icons/note.gif); +} + +.Icon.thread { + font-weight: bold; + font-size: 1em; + background-image: url(../images/icons/thread.gif); +} + + +.Icon.debugLog { background:url(../images/icons/log_debug.gif) 0 2px no-repeat; } +.Icon.alertLog { background:url(../images/icons/log_alert.gif) 0 2px no-repeat; } +.Icon.errorLog { background:url(../images/icons/log_error.gif) 0 2px no-repeat; } + + + +#content { + clear:both; + border:1px solid #aaa; + border-top:none; + border-bottom:3px solid #bbb; + padding:10px 10px 20px 10px; + background:#fff; +} + +#content a { + color:#184E81; +} + +#footer { + clear:both; + padding:10px; + text-align:center; + font-size:9pt; +} + +table { vertical-align:top; } + +table.list { + clear:both; + background:#ccc; + margin: 2px 0; + border-bottom: 1px solid #ccc; + font-family:arial, helvetica, sans-serif; + font-size:10pt; +} + +table.list caption { + text-align:left; + padding:5px; + background:#929292; + color:#fff; + font-weight:bold; +} + +table.list thead th { + background-color:#eee; + color:#000; + text-align:left; + vertical-align:top; +} + +table.list th a { + + text-decoration:none; + color:#000; +} + +table.list thead th a { padding: 3px; display: block; color: #000; background: url('../images/asc_desc.gif') 100% 50% no-repeat; } + +table.list thead th a.asc { background: url('../images/asc.gif') 100% 50% no-repeat #cfe6ff; } +table.list thead th a.desc { background: url('../images/desc.gif') 100% 50% no-repeat #cfe6ff; } +table.list tbody td { + background:#fff; + border:1px solid #fff; + padding:1px; + vertical-align:top; +} + +table.list tbody td { background: #fff; padding: 1px; padding-left:2px; vertical-align: top; } +table.list tbody tr.odd td { background-color: #f0faff; } +table.list tbody tr:hover td { background: #ffe; } +table.list tbody tr.odd:hover td { background: #ffd; } + +table.list tfoot td { + background:#eee; + padding: 2px; +} + +table.list tbody td.webticket, table.list tbody tr.row1 td.webticket { + text-indent:20px; + background:url(../images/icons/ticket_source_web.gif) 0 50% no-repeat #fff; +} + +table.list tbody td.emailticket, table.list tbody tr.row1 td.emailticket { + text-indent:20px; + background:url(../images/icons/ticket_source_email.gif) 0 50% no-repeat; +} + +table.list tbody td.phoneticket, table.list tbody tr.row1 td.phoneticket { + text-indent:20px; + background:url(../images/icons/ticket_source_phone.gif) 0 50% no-repeat; +} + +table.list tbody td.otherticket, table.list tbody tr.row1 td.otherticket { + text-indent:20px; + background:url(../images/icons/ticket_source_other.gif) 0 50% no-repeat; +} + +a.refresh { + display:block; + float:right; + width:auto; + height:16px; + line-height:16px; + padding:2px 5px 2px 2px; + background-position:2px 50%; + background-repeat:no-repeat; + padding-left:24px; + margin-left:10px; + margin-bottom: 2px; + border:1px solid #aaa; + background-image:url(../images/icons/refresh.gif); +} + +a.edit, a.print { + display:block; + float:right; + width:auto; + height:16px; + line-height:16px; + padding:2px 5px 2px 2px; + background-position:2px 50%; + background-repeat:no-repeat; + padding-left:24px; + margin-left:10px; + border:1px solid #aaa; + background-image:url(../images/icons/edit_ticket.png); +} + +a.print { + background-image:url(../images/icons/printer.gif); +} + +.btn { + padding:3px 10px; + background:url(../images/btn_bg.png) top left repeat-x #ccc; + border:1px solid #777; + color:#000; +} + +.button { padding:1px 5px; margin-right:10px; color:#777; font-weight:bold;} + +.btn_sm { + padding:2px 5px; + font-size:9pt; + background:url(../images/btn_sm_bg.png) top left repeat-x #f90; + border:1px solid #777; + color:#fff; + font-weight:bold; +} + +.btn:hover, .btn_sm:hover { + background-position: bottom left; +} + +.search label { + display:block; + line-height:25px; + height:25px; +} + +.search input[type=text] { + height:23px; + line-height:23px; + border:1px solid #aaa; + background:#fff; + padding:2px; +} + +.form_table { + margin-top:3px; + border-left:1px solid #ddd; + border-right:1px solid #ddd; +} + +.form_table td { + border-bottom:1px solid #ddd; +} + + +.form_table td.multi-line { + vertical-align:top; +} + +.form_table input[type=text], .form_table input[type=password], .form_table textarea { + background:#fff; + border:1px solid #aaa; +} + +.form_table input[type=radio], .form_table input[type=checkbox] { + position:relative; + top:3px; + margin-left:0; + padding-left:0; +} + +.form_table .required { + font-weight:bold; +} + +.form_table em { + font-weight:normal; + color:#666; +} + +.error { + color:#f00; +} + +.form_table .error input { + border:1px solid #f00; +} + +.form_table th { + text-align:left; + border:1px solid #ccc; + background:#eee; + padding:0; +} + +.form_table th h4 { + margin:0; + padding:5px; + color:#fff; + background:#929292; +} + +.form_table th em { + display:block; + padding:5px; + color:#000; +} + +.settings_table { + margin-top:2px; + border-left:1px solid #ddd; + border-right:1px solid #ddd; +} + +.settings_table td { + border-bottom:1px solid #ddd; +} + +.settings_table input[type=radio], .settings_table input[type=checkbox] { + margin-left:0; + padding-left:0; +} + +#content .settings_table th h4 a { + display:block; + color:#fff; +} + +.settings_table h4 a span { + font-size:12pt; + line-height:14px; + display:inline-block; + width:14px; + height:14px; + overflow:hidden; + text-align:center; + color:#444; + background:#ccc; + position:relative; + top:2px; +} + +h2 { + margin:0; + padding:0; + font-size:12pt; + color:#0A568E; +} + +h2 span { color:#000; } + +h3 { + margin:10px 0 0 0; + padding:5px 0; + font-size:10pt; + color:#444; +} + +.ticket_info th { + text-align:left; +} + +.ticket_info { + background:#F4FAFF; +} + +.right_align { text-align:right; } + +h2 .reload { + display:inline-block; + width:16px; + height:16px; + background:url(../images/icons/refresh.gif) top left no-repeat; + outline:none; + text-indent:-9999px; +} + +#assigned_message { + margin:10px 0; + padding:5px 5px 5px 30px; + background:url(../images/icons/assigned_ticket.gif) 5px 50% no-repeat #ffd; + border:1px solid #f90; +} + + + + +#ticket_actions { + padding:5px; + background:#eee; + border:1px solid #aaa; + border-bottom:none; + margin:0; +} + +#threads { + margin:0; + padding:5px 10px 0 10px; + border:1px solid #aaa; + background:#F4FAFF; + height:30px; +} + +#threads li { + list-style:none; + margin:0; + padding:0; + display:inline; +} + +#threads li a { + display:block; + width:auto; + float:left; + height:30px; + line-height:30px; + border-top:1px solid #F4FAFF; + padding:0 10px 0 32px; + margin-right:10px; +} + +#threads li a.active { + height:29px; + background-color:#fff; + border:1px solid #aaa; + border-bottom:none; + border-top:2px solid #ed9100; + font-weight:bold; +} + +#toggle_ticket_thread { + background:url(../images/icons/open.gif) 10px 50% no-repeat; +} + +#toggle_notes { + background:url(../images/icons/note.gif) 10px 50% no-repeat; +} + +#latest_notes { + margin:10px 0; + padding:10px; + background:#ffe; + border:1px solid #e7e765; +} + +#latest_notes h3 { + margin:0 0 10px 0; + padding:0; + font-size:11pt; +} + +#latest_notes h3 span, #latest_notes h3 a { + color:#777; + font-weight:normal; + text-decoration:none; + font-size:10pt; +} + +#latest_notes ul { + margin:0 20px; + padding:0; +} + +#latest_notes ul li { + margin:0; + padding:0 0 10px 0; + list-style:none; +} + +#latest_notes em { + color:#777; +} + +#ticket_thread table { + margin-top:10px; + border:1px solid #aaa; + border-bottom:2px solid #aaa; +} + +#ticket_notes table { + margin-top:10px; + border:1px solid #ddd; + border-bottom:2px solid #ddd; +} + +#ticket_thread table th, #ticket_notes table th { + text-align:left; + border-bottom:1px solid #aaa; + font-size:10pt; + padding:5px; +} + +#ticket_notes table th { + text-align:left; + border-bottom:1px solid #ddd; + font-size:10pt; + padding:5px; + background:#F4FAFF; +} + +#ticket_notes table th em { + font-weight:normal; + font-size:10pt; + color:#666; +} + +#ticket_thread .message th { + background:#C3D9FF; +} + +#ticket_notes .date { + font-weight:normal; + font-size:10pt; + color:#888; + text-align:right; +} + +#ticket_thread .response th { + background:#FFE0B3; +} + +#ticket_thread table td, #ticket_notes table td { + padding:5px; +} + +#ticket_notes td { + background:#f9f9f9; +} + +#ticket_thread .info, #ticket_notes .info { + padding:5px; + background:#F4FAFF; + height:16px; + line-height:16px; +} + +#ticket_notes .info { + background:#f9f9f9; +} + +#response_options { + margin-top:30px; +} + +#response_options form { + padding:0 10px; +} + +#response_options ul { + padding:4px 0 0 190px; + margin:0; + text-align:center; + height:29px; + border-bottom:1px solid #aaa; + background:#eef3f8; +} + +#response_options li { + margin:0; + padding:0; + display:inline; + list-style:none; +} + +#response_options li a { + width:130px; + font-weight:bold; + padding:5px; + height:18px; + line-height:20px; + color:#444; + display:block; + float:left; + outline:none; + position:relative; + top:0; + background:#fbfbfb; + border:1px solid #eee; + border-bottom:none; +} + +#response_options .reply_tab.tell { + color:#a00 !important; + background-image:url(../images/reminder.png); + background-position:12px 50%; + background-repeat:no-repeat; +} + +#response_options li a.active { + height:18px; + color:#184E81; + background-color:#f9f9f9; + border:1px solid #aaa; + border-top:2px solid #81a9d7; + border-bottom:none; +} + +#response_options form { + padding:10px 5px; + background:#f9f9f9; + border:1px solid #aaa; + border-top:none; +} + +#response_options table { + width:928px; +} + +#response_options td { + vertical-align:top; +} + +#response_options textarea { + width:760px !important; +} + +#response_options input[type=text], #response_options textarea { + border:1px solid #aaa; + background:#fff; +} + +.attachments .uploads div { + display:inline-block; + padding-right:20px; +} + + + +.file { + display:inline-block; + padding-left:20px; + margin-right:20px; + background:url(../images/icons/file.gif) 0 50% no-repeat; +} + +.expander { + line-height:14px; + display:inline-block; + width:12px; + height:12px; + overflow:hidden; + text-align:center; + color:#aaa; + position:relative; +} + +/** Popup Tool Tips and Content **/ + +.tip_box { + display:block; + height:30px; + position:absolute; + z-index:1000; +} + +.tip_arrow { + display:block; + position:absolute; + top:5px; + left:-11px; + width:12px; + z-index:700; +} + +.tip_content { + height:auto !important; + height:20px; + min-height:20px; + padding:10px 5px 5px 5px; + border:1px solid #666; + background:#fff; + -moz-border-radius:5px; + -webkit-border-radius:5px; + border-radius:5px; + -moz-box-shadow: 3px 3px 3px #666; + -webkit-box-shadow: 3px 3px 3px #666; + box-shadow: 3px 3px 3px #666; + z-index:500; + position:absolute; + top:0; + left:-1px; + width:auto !important; + width:300px; +} + +.tip_close { + position:absolute; + left:100%; + top:0; + margin-left:-12px; +} + +.tip_shadow { + display:none; + background:#000; + filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius=3,MakeShadow=true,ShadowOpacity=0.60); + -ms-filter: "progid:DXImageTransform.Microsoft.Blur(PixelRadius=3,MakeShadow=true,ShadowOpacity=0.60)"; + zoom: 1; + position:absolute; + z-index:200; + top:0; + left:0; + width:auto !important; + width:310px; +} + +.tip_menu { + margin:10px 0 0 0; + padding:5px 0; + border-top:1px solid #ddd; + height:16px; + font-size:9pt; +} + +.tip_menu li { + display:inline; + list-style:none; + margin:0; + padding:0; +} + +.tip_menu li a { + display:block; + width:auto; + _width:0; + float:left; + padding:0 10px; + border-right:1px solid #ddd; + color:#666; +} + +.tip_menu li a:hover { + color:#E76C74; +} + +.tip_content form { + display:none; + line-height:24px; +} + +.tip_content select, .tip_content textarea { + width:295px; +} + +.tip_content textarea { + padding:0; + border:1px solid #aaa; + background:#fff; +} + +.tip_content form p { + margin:0; + width:auto !important; + width:295px; + text-align:right; + line-height:24px; +} + +/* Knowledgebase */ +#kb { + margin: 2px 0; + padding: 0; + overflow: hidden; +} + +#kb > li { + margin: 0 0 5px 0; + padding: 0 10px; + width: auto; + float: left; + clear: both; +} + +#kb > li h4 { + padding-bottom:3px; + margin-bottom:3px; +} + +#kb > li h4 span { + color:#666; +} + +#kb > li h4 a { + font-size: 14px; +} + +#faq { + clear: both; + margin: 0; + padding: 5 0 10px 5px; +} +#faq ol { + font-size: 15px; + margin-left: 0; + padding-left: 0; +} +#faq ol li { + list-style: none; + margin: 0 0 10px 0; + color: #999; +} +#faq ol li a { + display: inline; + height: 16px; + line-height: 16px; + padding-left: 24px; + background: url('../images/icons/page.png') 0 50% no-repeat; +} diff --git a/scp/dashboard.php b/scp/dashboard.php new file mode 100644 index 0000000000000000000000000000000000000000..0551639566396dea6544b361b22a41b7b21ce0db --- /dev/null +++ b/scp/dashboard.php @@ -0,0 +1,22 @@ +<?php +/********************************************************************* + dashboard.php + + Staff's Dashboard - basic stats...etc. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +$nav->setTabActive('dashboard'); +require(STAFFINC_DIR.'header.inc.php'); +//require(STAFFINC_DIR.$page); +echo "Staff's dashboard"; +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/departments.php b/scp/departments.php new file mode 100644 index 0000000000000000000000000000000000000000..d0869cdf7302cafc8fb8fff071b28291833e4120 --- /dev/null +++ b/scp/departments.php @@ -0,0 +1,107 @@ +<?php +/********************************************************************* + departments.php + + Departments + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +$dept=null; +if($_REQUEST['id'] && !($dept=Dept::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid department ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$dept){ + $errors['err']='Unknown or invalid department.'; + }elseif($dept->update($_POST,$errors)){ + $msg='Department updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating department. Try again!'; + } + break; + case 'create': + if(($id=Dept::create($_POST,$errors))){ + $msg=Format::htmlchars($_POST['name']).' added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add department. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one department'; + }elseif(!$_POST['public'] && in_array($cfg->getDefaultDeptId(),$_POST['ids'])) { + $errors['err']='You can not disable/delete a default department. Remove default Dept. and try again.'; + }else{ + $count=count($_POST['ids']); + if($_POST['public']){ + $sql='UPDATE '.DEPT_TABLE.' SET ispublic=1 WHERE dept_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected departments made public'; + else + $warn="$num of $count selected departments made public"; + }else{ + $errors['err']='Unable to make selected department public.'; + } + }elseif($_POST['private']){ + $sql='UPDATE '.DEPT_TABLE.' SET ispublic=0 '. + 'WHERE dept_id IN ('.implode(',',$_POST['ids']).') AND dept_id!='.db_input($cfg->getDefaultDeptId()); + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected departments made private'; + else + $warn="$num of $count selected departments made private"; + }else{ + $errors['err']='Unable to make selected department(s) private. Possibly already private!'; + } + + }elseif($_POST['delete']){ + //Deny all deletes if one of the selections has members in it. + $sql='SELECT count(staff_id) FROM '.STAFF_TABLE.' WHERE dept_id IN ('.implode(',',$_POST['ids']).')'; + list($members)=db_fetch_row(db_query($sql)); + if($members) + $errors['err']='Dept. with users can not be deleted. Move staff first.'; + else{ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if($v!=$cfg->getDefaultDeptId() && ($d=Dept::lookup($v)) && $d->delete()) + $i++; + } + if($i && $i==$count) + $msg='Selected departments deleted successfully'; + elseif($i>0) + $warn="$i of $count selected departments deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected departments.'; + } + }else { + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='departments.inc.php'; +if($dept || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='department.inc.php'; + +$nav->setTabActive('depts'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/directory.php b/scp/directory.php new file mode 100644 index 0000000000000000000000000000000000000000..db5b64ce10f88171b01c40f03e3e04e4e89d8275 --- /dev/null +++ b/scp/directory.php @@ -0,0 +1,22 @@ +<?php +/********************************************************************* + directory.php + + Staff directory + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +$page='directory.inc.php'; +$nav->setTabActive('dashboard'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/emails.php b/scp/emails.php new file mode 100644 index 0000000000000000000000000000000000000000..ddc626fb36dc9265b41835a95dbc6bf7c676e96b --- /dev/null +++ b/scp/emails.php @@ -0,0 +1,86 @@ +<?php +/********************************************************************* + emails.php + + Emails + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.email.php'); + +$email=null; +if($_REQUEST['id'] && !($email=Email::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid email ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$email){ + $errors['err']='Unknown or invalid email.'; + }elseif($email->update($_POST,$errors)){ + $msg='Email updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating email. Try again!'; + } + break; + case 'create': + if(($id=Email::create($_POST,$errors))){ + $msg='Email address added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add email. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one email address'; + }else{ + $count=count($_POST['ids']); + + $sql='SELECT count(dept_id) FROM '.DEPT_TABLE.' dept '. + 'WHERE email_id IN ('.implode(',',$_POST['ids']).') OR autoresp_email_id IN ('.implode(',',$_POST['ids']).')'; + list($depts)=db_fetch_row(db_query($sql)); + if($depts>0){ + $errors['err']='One or more of the selected emails is being used by a department. Remove association first!'; + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if($v!=$cfg->getDefaultEmailId() && ($e=Email::lookup($v)) && $e->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected emails deleted successfully'; + elseif($i>0) + $warn="$i of $count selected emails deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected emails'; + + }else { + $errors['err']='Unknown command'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='emails.inc.php'; +if($email || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='email.inc.php'; + +$nav->setTabActive('emails'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/emailtest.php b/scp/emailtest.php new file mode 100644 index 0000000000000000000000000000000000000000..9f9b2decb0a3b5e4cde81b3b4ca1ce4ec853a0f5 --- /dev/null +++ b/scp/emailtest.php @@ -0,0 +1,120 @@ +<?php +/********************************************************************* + emailtest.php + + Email Diagnostic + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.email.php'); +$info=array(); +$info['subj']='osTicket test email'; + +if($_POST){ + $errors=array(); + $email=null; + if(!$_POST['email_id'] || !($email=Email::lookup($_POST['email_id']))) + $errors['email_id']='Select from email address'; + + if(!$_POST['email'] || !Validator::is_email($_POST['email'])) + $errors['email']='To email address required'; + + if(!$_POST['subj']) + $errors['subj']='Subject required'; + + if(!$_POST['message']) + $errors['message']='Message required'; + + if(!$errors && $email){ + if($email->send($_POST['email'],$_POST['subj'],$_POST['message'])) + $msg='Test email sent successfully to '.Format::htmlchars($_POST['email']); + else + $errors['err']='Error sending email - try again.'; + }elseif($errors['err']){ + $errors['err']='Error sending email - try again.'; + } +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +$nav->setTabActive('emails'); +require(STAFFINC_DIR.'header.inc.php'); +?> +<form action="emailtest.php" method="post" id="emailtest"> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <h2>Test Outgoing Email</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <em>Emails delivery depends on your server settings (php.ini) and/or email SMTP configuration.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="120" class="required"> + From: + </td> + <td> + <select name="email_id"> + <option value="0">— Select FROM Email —</option> + <?php + $sql='SELECT email_id,email,name,smtp_active FROM '.EMAIL_TABLE.' email ORDER by name'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$email,$name,$smtp)=db_fetch_row($res)){ + $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':''; + if($name) + $email=Format::htmlchars("$name <$email>"); + if($smtp) + $email.=' (SMTP)'; + + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email); + } + } + ?> + </select> + <span class="error">* <?php echo $errors['email_id']; ?></span> + </td> + </tr> + <tr> + <td width="120" class="required"> + To: + </td> + <td> + <input type="text" size="60" name="email" value="<?php echo $info['email']; ?>"> + <span class="error">* <?php echo $errors['email']; ?></span> + </td> + </tr> + <tr> + <td width="120" class="required"> + Subject: + </td> + <td> + <input type="text" size="60" name="subj" value="<?php echo $info['subj']; ?>"> + <span class="error">* <?php echo $errors['subj']; ?></span> + </td> + </tr> + <tr> + <td colspan=2> + <em><strong>Message</strong>: email message to send.</em> <span class="error">* <?php echo $errors['message']; ?></span><br> + <textarea name="message" cols="21" rows="10" style="width: 90%;"><?php echo $info['message']; ?></textarea> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="Send Message"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="emails.php"'> +</p> +</form> +<?php +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/faq.php b/scp/faq.php new file mode 100644 index 0000000000000000000000000000000000000000..f668be9f233fe5f2678bebceda1f6fda94dfdd13 --- /dev/null +++ b/scp/faq.php @@ -0,0 +1,114 @@ +<?php +/********************************************************************* + faq.php + + FAQs. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.faq.php'); + +$faq=$category=null; +if($_REQUEST['id'] && !($faq=FAQ::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid FAQ'; + +if($_REQUEST['cid'] && !$faq && !($category=Category::lookup($_REQUEST['cid']))) + $errors['err']='Unknown or invalid FAQ category'; + +if($_POST): + $errors=array(); + switch(strtolower($_POST['do'])) { + case 'create': + case 'add': + if(($faq=FAQ::add($_POST,$errors))) + $msg='FAQ added successfully'; + elseif(!$errors['err']) + $errors['err'] = 'Unable to add FAQ. Try again!'; + break; + case 'update': + case 'edit'; + if(!$faq) + $errors['err'] = 'Invalid or unknown FAQ'; + elseif($faq->update($_POST,$errors)) { + $msg='FAQ updated successfully'; + $_REQUEST['a']=null; //Go back to view + //Delete removed attachments. + $keepers = $_POST['files']?$_POST['files']:array(); + if(($attachments = $faq->getAttachments())) { + foreach($attachments as $k=>$file) { + if($file['id'] && !in_array($file['id'], $keepers)) { + $faq->deleteAttachment($file['id']); + } + } + } + //Upload NEW attachments IF ANY - TODO: validate attachment types?? + if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + $faq->uploadAttachments($files); + + } elseif(!$errors['err']) + $errors['err'] = 'Unable to update FAQ. Try again!'; + break; + case 'manage-faq': + if(!$faq) { + $errors['err']='Unknown or invalid FAQ'; + } else { + switch(strtolower($_POST['a'])) { + case 'edit': + $_GET['a']='edit'; + break; + case 'publish'; + if($faq->publish()) + $msg='FAQ published successfully'; + else + $errors['err']='Unable to publish the FAQ. Try editing it.'; + break; + case 'unpublish'; + if($faq->unpublish()) + $msg='FAQ unpublished successfully'; + else + $errors['err']='Unable to unpublish the FAQ. Try editing it.'; + break; + case 'delete': + $category = $faq->getCategory(); + if($faq->delete()) { + $msg='FAQ deleted successfully'; + $faq=null; + } else { + $errors['err']='Unable to delete FAQ. Try again'; + } + break; + default: + $errors['err']='Invalid action'; + } + } + break; + default: + $errors['err']='Unknown action'; + + } +endif; + + +$inc='faq-categories.inc.php'; //FAQs landing page. +if($faq) { + $inc='faq-view.inc.php'; + if($_REQUEST['a']=='edit' && $thisstaff->canManageFAQ()) + $inc='faq.inc.php'; +}elseif($_REQUEST['a']=='add' && $thisstaff->canManageFAQ()) { + $inc='faq.inc.php'; +} elseif($category && $_REQUEST['a']!='search') { + $inc='faq-category.inc.php'; +} +$nav->setTabActive('kbase'); +require_once(STAFFINC_DIR.'header.inc.php'); +require_once(STAFFINC_DIR.$inc); +require_once(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/file.php b/scp/file.php new file mode 100644 index 0000000000000000000000000000000000000000..61bc7da1944cc60cddb36b6983b7d1b4eaf03390 --- /dev/null +++ b/scp/file.php @@ -0,0 +1,30 @@ +<?php +/********************************************************************* + file.php + + Simply downloads the file...on hash validation as follows; + + * Hash must be 64 chars long. + * First 32 chars is the perm. file hash + * Next 32 chars is md5(file_id.session_id().file_hash) + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.file.php'); +$h=trim($_GET['h']); +//basic checks +if(!$h || strlen($h)!=64 //32*2 + || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. + || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getHash()))) //next 32 is file id + session hash. + die('Unknown or invalid file. #'.Format::htmlchars($_GET['h'])); + +$file->download(); +?> diff --git a/scp/filters.php b/scp/filters.php new file mode 100644 index 0000000000000000000000000000000000000000..f39b794d047a1f4fa756c320346dae26ed8b9623 --- /dev/null +++ b/scp/filters.php @@ -0,0 +1,104 @@ +<?php +/********************************************************************* + filters.php + + Email Filters + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.filter.php'); +$filter=null; +if($_REQUEST['id'] && !($filter=Filter::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid filter.'; + +/* NOTE: Banlist has its own interface*/ +if($filter && $filter->isSystemBanlist()) + header('Location: banlist.php'); + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$filter){ + $errors['err']='Unknown or invalid filter.'; + }elseif($filter->update($_POST,$errors)){ + $msg='Filter updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating filter. Try again!'; + } + break; + case 'add': + if((Filter::create($_POST,$errors))){ + $msg='Filter added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add filter. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one filter to process.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.EMAIL_FILTER_TABLE.' SET isactive=1 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected filters enabled'; + else + $warn="$num of $count selected filters enabled"; + }else{ + $errors['err']='Unable to enable selected filters'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.EMAIL_FILTER_TABLE.' SET isactive=0 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected filters disabled'; + else + $warn="$num of $count selected filters disabled"; + }else{ + $errors['err']='Unable to disable selected filters'; + } + + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($f=Filter::lookup($v)) && !$f->isSystemBanlist() && $f->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected filters deleted successfully'; + elseif($i>0) + $warn="$i of $count selected filters deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected filters'; + + }else { + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='filters.inc.php'; +if($filter || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='filter.inc.php'; + +$nav->setTabActive('emails'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/groups.php b/scp/groups.php new file mode 100644 index 0000000000000000000000000000000000000000..7a4f959169160f6f41f0ebce70db6c6ced266695 --- /dev/null +++ b/scp/groups.php @@ -0,0 +1,96 @@ +<?php +/********************************************************************* + groups.php + + User Groups. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +$group=null; +if($_REQUEST['id'] && !($group=Group::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid group ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$group){ + $errors['err']='Unknown or invalid group.'; + }elseif($group->update($_POST,$errors)){ + $msg='Group updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Unable to update group. Correct any error(s) below and try again!'; + } + break; + case 'create': + if(($id=Group::create($_POST,$errors))){ + $msg=Format::htmlchars($_POST['name']).' added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add group. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one group.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=1, updated=NOW() WHERE group_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected groups activated'; + else + $warn="$num of $count selected groups activated"; + }else{ + $errors['err']='Unable to activate selected groups'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=0, updated=NOW() WHERE group_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected groups disabled'; + else + $warn="$num of $count selected groups disabled"; + }else{ + $errors['err']='Unable to disable selected groups'; + } + }elseif($_POST['delete']){ + foreach($_POST['ids'] as $k=>$v) { + if(($t=Group::lookup($v)) && $t->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected groups deleted successfully'; + elseif($i>0) + $warn="$i of $count selected groups deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected groups'; + }else{ + $errors['err']='Unknown action. Get technical help!'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='groups.inc.php'; +if($group || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='group.inc.php'; + +$nav->setTabActive('staff'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/helptopics.php b/scp/helptopics.php new file mode 100644 index 0000000000000000000000000000000000000000..18c34393e9b89a67c91661b516f0f045273d34da --- /dev/null +++ b/scp/helptopics.php @@ -0,0 +1,101 @@ +<?php +/********************************************************************* + helptopics.php + + Help Topics. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.topic.php'); + +$topic=null; +if($_REQUEST['id'] && !($topic=Topic::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid help topic ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$topic){ + $errors['err']='Unknown or invalid help topic.'; + }elseif($topic->update($_POST,$errors)){ + $msg='Help topic updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating help topic. Try again!'; + } + break; + case 'create': + if(($id=Topic::create($_POST,$errors))){ + $msg='Help topic added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add help topic. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one help topic'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.TOPIC_TABLE.' SET isactive=1 WHERE topic_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected help topics enabled'; + else + $warn="$num of $count selected help topics enabled"; + }else{ + $errors['err']='Unable to enable selected help topics.'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.TOPIC_TABLE.' SET isactive=0 WHERE topic_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected help topics disabled'; + else + $warn="$num of $count selected help topics disabled"; + }else{ + $errors['err']='Unable to disable selected help topic(s)'; + } + + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=Topic::lookup($v)) && $t->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected help topics deleted successfully'; + elseif($i>0) + $warn="$i of $count selected help topics deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected help topics'; + + }else { + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='helptopics.inc.php'; +if($topic || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='helptopic.inc.php'; + +$nav->setTabActive('topics'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/images/arrowleftmonth.gif b/scp/images/arrowleftmonth.gif new file mode 100644 index 0000000000000000000000000000000000000000..313b627354c54cc90f37ce9da55c42105f0e2a18 Binary files /dev/null and b/scp/images/arrowleftmonth.gif differ diff --git a/scp/images/arrowrightmonth.gif b/scp/images/arrowrightmonth.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad2aa18b8fbe4d085ec7f235ae15f3cf5321389 Binary files /dev/null and b/scp/images/arrowrightmonth.gif differ diff --git a/scp/images/asc.gif b/scp/images/asc.gif new file mode 100644 index 0000000000000000000000000000000000000000..3b30b3c58eabdb47a1c420ad03c8e30b966cc858 Binary files /dev/null and b/scp/images/asc.gif differ diff --git a/scp/images/asc_desc.gif b/scp/images/asc_desc.gif new file mode 100644 index 0000000000000000000000000000000000000000..fac668fcf42af844a3af0a239fa638ddbc08443c Binary files /dev/null and b/scp/images/asc_desc.gif differ diff --git a/scp/images/btn_bg.png b/scp/images/btn_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..322d0b6e129149f6bcb100c300989596359f7f5f Binary files /dev/null and b/scp/images/btn_bg.png differ diff --git a/scp/images/btn_sm_bg.png b/scp/images/btn_sm_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..819ffbb63d91323cb131cd4428bad94cd691d93e Binary files /dev/null and b/scp/images/btn_sm_bg.png differ diff --git a/scp/images/cal.gif b/scp/images/cal.gif new file mode 100644 index 0000000000000000000000000000000000000000..8526cf5d19a915aa8073cf344873c4505491970d Binary files /dev/null and b/scp/images/cal.gif differ diff --git a/scp/images/cal.png b/scp/images/cal.png new file mode 100644 index 0000000000000000000000000000000000000000..6cff76c1dbb71dfe2fcf35dec28cdf935ebc945d Binary files /dev/null and b/scp/images/cal.png differ diff --git a/scp/images/desc.gif b/scp/images/desc.gif new file mode 100644 index 0000000000000000000000000000000000000000..74157867f25acbc146704d43399d6c3605ba7724 Binary files /dev/null and b/scp/images/desc.gif differ diff --git a/scp/images/header-bg.png b/scp/images/header-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..dc3e1d5c51f1a9c7b58c5a5b8c423a0152c72767 Binary files /dev/null and b/scp/images/header-bg.png differ diff --git a/scp/images/icons/add.png b/scp/images/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..d1f6a4006047e797b78790c06eadb71cc0905d3b Binary files /dev/null and b/scp/images/icons/add.png differ diff --git a/scp/images/icons/add_new_email.gif b/scp/images/icons/add_new_email.gif new file mode 100644 index 0000000000000000000000000000000000000000..620553e98dd64d7510ce4d1e91b5057ed96fcd52 Binary files /dev/null and b/scp/images/icons/add_new_email.gif differ diff --git a/scp/images/icons/alert.png b/scp/images/icons/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..8892a55a4ef40387dd9a210f581d3c6f779a1114 Binary files /dev/null and b/scp/images/icons/alert.png differ diff --git a/scp/images/icons/answered.gif b/scp/images/icons/answered.gif new file mode 100644 index 0000000000000000000000000000000000000000..2defcfcf3be9baf93dba0260e73a2b2b3770f09f Binary files /dev/null and b/scp/images/icons/answered.gif differ diff --git a/scp/images/icons/answered_tickets.gif b/scp/images/icons/answered_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..2defcfcf3be9baf93dba0260e73a2b2b3770f09f Binary files /dev/null and b/scp/images/icons/answered_tickets.gif differ diff --git a/scp/images/icons/api.png b/scp/images/icons/api.png new file mode 100644 index 0000000000000000000000000000000000000000..6c21c678885be77593864d1b8edd73cb75d76604 Binary files /dev/null and b/scp/images/icons/api.png differ diff --git a/scp/images/icons/api_settings.gif b/scp/images/icons/api_settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..9d5b44eace621a51a6aa4c235df4af6ee3be64ff Binary files /dev/null and b/scp/images/icons/api_settings.gif differ diff --git a/scp/images/icons/articles.png b/scp/images/icons/articles.png new file mode 100644 index 0000000000000000000000000000000000000000..9fda911aea208b9d9f5879c04b410eb9c6da165f Binary files /dev/null and b/scp/images/icons/articles.png differ diff --git a/scp/images/icons/assigned_ticket.gif b/scp/images/icons/assigned_ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..6d81d33309fab82e65316842d20700bb97d1f872 Binary files /dev/null and b/scp/images/icons/assigned_ticket.gif differ diff --git a/scp/images/icons/assigned_tickets.gif b/scp/images/icons/assigned_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..373b8d58d4efb06c984ca6bdff5d19dd28e02023 Binary files /dev/null and b/scp/images/icons/assigned_tickets.gif differ diff --git a/scp/images/icons/attachment.gif b/scp/images/icons/attachment.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ea1516e430b3ec558cb6884ed36ddfcb29def32 Binary files /dev/null and b/scp/images/icons/attachment.gif differ diff --git a/scp/images/icons/ban_list.gif b/scp/images/icons/ban_list.gif new file mode 100644 index 0000000000000000000000000000000000000000..c1ac87932aa28c41a6a96e43f8fb30e93295d998 Binary files /dev/null and b/scp/images/icons/ban_list.gif differ diff --git a/scp/images/icons/cancel.png b/scp/images/icons/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..bff388fe3061cc151c420563125502c361a0101b Binary files /dev/null and b/scp/images/icons/cancel.png differ diff --git a/scp/images/icons/change_password.gif b/scp/images/icons/change_password.gif new file mode 100644 index 0000000000000000000000000000000000000000..df7b54d2dd0e3460ae1b98143f5b08a4dd73a32a Binary files /dev/null and b/scp/images/icons/change_password.gif differ diff --git a/scp/images/icons/closed.gif b/scp/images/icons/closed.gif new file mode 100644 index 0000000000000000000000000000000000000000..b2c0deaa9dfc5a2d960fe00095f6d80f2bcf0424 Binary files /dev/null and b/scp/images/icons/closed.gif differ diff --git a/scp/images/icons/closed_tickets.gif b/scp/images/icons/closed_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..b2c0deaa9dfc5a2d960fe00095f6d80f2bcf0424 Binary files /dev/null and b/scp/images/icons/closed_tickets.gif differ diff --git a/scp/images/icons/date.png b/scp/images/icons/date.png new file mode 100644 index 0000000000000000000000000000000000000000..25fd5d32b29823ab2dd9648c55d1f79fe9882f82 Binary files /dev/null and b/scp/images/icons/date.png differ diff --git a/scp/images/icons/delete.png b/scp/images/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..7038ed297e7d350110626e00d3304841e19cfa29 Binary files /dev/null and b/scp/images/icons/delete.png differ diff --git a/scp/images/icons/edit.png b/scp/images/icons/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..44ab26dd9017830d682818526d18f5269230e164 Binary files /dev/null and b/scp/images/icons/edit.png differ diff --git a/scp/images/icons/edit_ticket.png b/scp/images/icons/edit_ticket.png new file mode 100644 index 0000000000000000000000000000000000000000..632867e20c9d1f150e0acbb393bbb765e16acba0 Binary files /dev/null and b/scp/images/icons/edit_ticket.png differ diff --git a/scp/images/icons/email_diagnostic.gif b/scp/images/icons/email_diagnostic.gif new file mode 100644 index 0000000000000000000000000000000000000000..f8e05f6e54ed2077ec9e99b56d30118905e4de5b Binary files /dev/null and b/scp/images/icons/email_diagnostic.gif differ diff --git a/scp/images/icons/email_filters.png b/scp/images/icons/email_filters.png new file mode 100644 index 0000000000000000000000000000000000000000..dc4e198b63e4433350daa91a5a3bf0591f2b8eec Binary files /dev/null and b/scp/images/icons/email_filters.png differ diff --git a/scp/images/icons/email_templates.gif b/scp/images/icons/email_templates.gif new file mode 100644 index 0000000000000000000000000000000000000000..9544721f8497db6b5c8696486d410de017c4bbff Binary files /dev/null and b/scp/images/icons/email_templates.gif differ diff --git a/scp/images/icons/email_templates.png b/scp/images/icons/email_templates.png new file mode 100644 index 0000000000000000000000000000000000000000..495e8b9de7f6f2807f8b52969e3ce18013309e61 Binary files /dev/null and b/scp/images/icons/email_templates.png differ diff --git a/scp/images/icons/emails.png b/scp/images/icons/emails.png new file mode 100644 index 0000000000000000000000000000000000000000..00579559bab848776d78cbcc505c3d41d74527a7 Binary files /dev/null and b/scp/images/icons/emails.png differ diff --git a/scp/images/icons/error.png b/scp/images/icons/error.png new file mode 100755 index 0000000000000000000000000000000000000000..a11afcfd53f37bbe6881198792fb21da1607cfc3 Binary files /dev/null and b/scp/images/icons/error.png differ diff --git a/scp/images/icons/file.gif b/scp/images/icons/file.gif new file mode 100644 index 0000000000000000000000000000000000000000..4400e61e9812a3b2a070c89fc6fce7489c104e17 Binary files /dev/null and b/scp/images/icons/file.gif differ diff --git a/scp/images/icons/help.gif b/scp/images/icons/help.gif new file mode 100644 index 0000000000000000000000000000000000000000..526d30a50b9875a0c95b4cf656d76ca24a178f67 Binary files /dev/null and b/scp/images/icons/help.gif differ diff --git a/scp/images/icons/help_topics.png b/scp/images/icons/help_topics.png new file mode 100644 index 0000000000000000000000000000000000000000..28dc32f3cbfa7e62ba1c3b34558946c8b371f9d9 Binary files /dev/null and b/scp/images/icons/help_topics.png differ diff --git a/scp/images/icons/kb-large.png b/scp/images/icons/kb-large.png new file mode 100644 index 0000000000000000000000000000000000000000..2676e91c5be4bd354720011be66501423c50bfdc Binary files /dev/null and b/scp/images/icons/kb-large.png differ diff --git a/scp/images/icons/list_departments.gif b/scp/images/icons/list_departments.gif new file mode 100644 index 0000000000000000000000000000000000000000..a4d178e52bdb93f72a0c55beb751f9560921cec1 Binary files /dev/null and b/scp/images/icons/list_departments.gif differ diff --git a/scp/images/icons/list_groups.gif b/scp/images/icons/list_groups.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c752a9be493ba6cb19e9122ea5a6395d7195f07 Binary files /dev/null and b/scp/images/icons/list_groups.gif differ diff --git a/scp/images/icons/list_users.gif b/scp/images/icons/list_users.gif new file mode 100644 index 0000000000000000000000000000000000000000..373b8d58d4efb06c984ca6bdff5d19dd28e02023 Binary files /dev/null and b/scp/images/icons/list_users.gif differ diff --git a/scp/images/icons/locked_ticket.gif b/scp/images/icons/locked_ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..619279d3690e993c76c2386df99e7165d7dd5cbe Binary files /dev/null and b/scp/images/icons/locked_ticket.gif differ diff --git a/scp/images/icons/log_alert.gif b/scp/images/icons/log_alert.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb52f61c8196f93d48c60525db74e3b3768c366c Binary files /dev/null and b/scp/images/icons/log_alert.gif differ diff --git a/scp/images/icons/log_debug.gif b/scp/images/icons/log_debug.gif new file mode 100644 index 0000000000000000000000000000000000000000..4b6fdece3350a36d0482abd3207d544d731abaa6 Binary files /dev/null and b/scp/images/icons/log_debug.gif differ diff --git a/scp/images/icons/log_error.gif b/scp/images/icons/log_error.gif new file mode 100644 index 0000000000000000000000000000000000000000..4914e85f66558458964bf7f6104bd79efb4ed2df Binary files /dev/null and b/scp/images/icons/log_error.gif differ diff --git a/scp/images/icons/logs.gif b/scp/images/icons/logs.gif new file mode 100644 index 0000000000000000000000000000000000000000..10a29ef0490733aaeee4f9b658cd1bb4962f021f Binary files /dev/null and b/scp/images/icons/logs.gif differ diff --git a/scp/images/icons/mine.gif b/scp/images/icons/mine.gif new file mode 100644 index 0000000000000000000000000000000000000000..32c941aac60932bd57665929b50dec57d53c9593 Binary files /dev/null and b/scp/images/icons/mine.gif differ diff --git a/scp/images/icons/my_tickets.gif b/scp/images/icons/my_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..32c941aac60932bd57665929b50dec57d53c9593 Binary files /dev/null and b/scp/images/icons/my_tickets.gif differ diff --git a/scp/images/icons/new.gif b/scp/images/icons/new.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a675850c713e6b6ea9eda5920c39e158c2f05f8 Binary files /dev/null and b/scp/images/icons/new.gif differ diff --git a/scp/images/icons/new_api.png b/scp/images/icons/new_api.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b060d1cbd8d81971ae159ac5ee7427de371959 Binary files /dev/null and b/scp/images/icons/new_api.png differ diff --git a/scp/images/icons/new_article.png b/scp/images/icons/new_article.png new file mode 100644 index 0000000000000000000000000000000000000000..e2eae94f2d8f9d9e9e7af999edfeb9448552de53 Binary files /dev/null and b/scp/images/icons/new_article.png differ diff --git a/scp/images/icons/new_department.gif b/scp/images/icons/new_department.gif new file mode 100644 index 0000000000000000000000000000000000000000..b856f4db3866420ad31d67aa3d9a0c492f2ccd83 Binary files /dev/null and b/scp/images/icons/new_department.gif differ diff --git a/scp/images/icons/new_email.png b/scp/images/icons/new_email.png new file mode 100644 index 0000000000000000000000000000000000000000..97b20e88d2ea0a03c28eac992cbef67e2590efe4 Binary files /dev/null and b/scp/images/icons/new_email.png differ diff --git a/scp/images/icons/new_email_filter.png b/scp/images/icons/new_email_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..a71518d757a8261cfdc9668a17fd5839834ce5ba Binary files /dev/null and b/scp/images/icons/new_email_filter.png differ diff --git a/scp/images/icons/new_email_template.png b/scp/images/icons/new_email_template.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1526c3818e3c4062e85ad838cc43520ea5dade Binary files /dev/null and b/scp/images/icons/new_email_template.png differ diff --git a/scp/images/icons/new_group.gif b/scp/images/icons/new_group.gif new file mode 100644 index 0000000000000000000000000000000000000000..e0fcdfe55901e25e66af7107d418cf08f0b6c84b Binary files /dev/null and b/scp/images/icons/new_group.gif differ diff --git a/scp/images/icons/new_help_topic.gif b/scp/images/icons/new_help_topic.gif new file mode 100644 index 0000000000000000000000000000000000000000..1782e843e3cb49f1ac3c7f3ed6f2437f9dd584af Binary files /dev/null and b/scp/images/icons/new_help_topic.gif differ diff --git a/scp/images/icons/new_help_topic.png b/scp/images/icons/new_help_topic.png new file mode 100644 index 0000000000000000000000000000000000000000..939353b7e559aa0dfcdc8cbd413fa4b4c05c37a0 Binary files /dev/null and b/scp/images/icons/new_help_topic.png differ diff --git a/scp/images/icons/new_premade_reply.gif b/scp/images/icons/new_premade_reply.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b01ff80c0489612c531b243fff36bf3fa2ac159 Binary files /dev/null and b/scp/images/icons/new_premade_reply.gif differ diff --git a/scp/images/icons/new_sla.png b/scp/images/icons/new_sla.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e5b0bae3d467d051967b37ba0dc730dae48c40 Binary files /dev/null and b/scp/images/icons/new_sla.png differ diff --git a/scp/images/icons/new_team.gif b/scp/images/icons/new_team.gif new file mode 100644 index 0000000000000000000000000000000000000000..eb913d137f1565d55f89e75ab69ecafc2da91b8c Binary files /dev/null and b/scp/images/icons/new_team.gif differ diff --git a/scp/images/icons/new_team.png b/scp/images/icons/new_team.png new file mode 100644 index 0000000000000000000000000000000000000000..552211ca89092a4d52f87776f7c0103602db541e Binary files /dev/null and b/scp/images/icons/new_team.png differ diff --git a/scp/images/icons/new_ticket.gif b/scp/images/icons/new_ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a675850c713e6b6ea9eda5920c39e158c2f05f8 Binary files /dev/null and b/scp/images/icons/new_ticket.gif differ diff --git a/scp/images/icons/new_user.gif b/scp/images/icons/new_user.gif new file mode 100644 index 0000000000000000000000000000000000000000..97478ca317a7023718dfc583ee64ce7eb11c4d89 Binary files /dev/null and b/scp/images/icons/new_user.gif differ diff --git a/scp/images/icons/note.gif b/scp/images/icons/note.gif new file mode 100644 index 0000000000000000000000000000000000000000..17b9f418541130eb083e78e219e538d0c6df4034 Binary files /dev/null and b/scp/images/icons/note.gif differ diff --git a/scp/images/icons/ok.png b/scp/images/icons/ok.png new file mode 100755 index 0000000000000000000000000000000000000000..7a5d21fdad11fe220217e8da11df8b0af947cae1 Binary files /dev/null and b/scp/images/icons/ok.png differ diff --git a/scp/images/icons/open.gif b/scp/images/icons/open.gif new file mode 100644 index 0000000000000000000000000000000000000000..83e025eba51423c2c4b92c0ffd04f4ffba9a2926 Binary files /dev/null and b/scp/images/icons/open.gif differ diff --git a/scp/images/icons/open_tickets.gif b/scp/images/icons/open_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..83e025eba51423c2c4b92c0ffd04f4ffba9a2926 Binary files /dev/null and b/scp/images/icons/open_tickets.gif differ diff --git a/scp/images/icons/overdue_ticket.gif b/scp/images/icons/overdue_ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..6f3067ff3e27f13743e1df59eb3758a691c91d7d Binary files /dev/null and b/scp/images/icons/overdue_ticket.gif differ diff --git a/scp/images/icons/overdue_tickets.gif b/scp/images/icons/overdue_tickets.gif new file mode 100644 index 0000000000000000000000000000000000000000..4d572a500445a6399b880eee957b7f8f56bc3874 Binary files /dev/null and b/scp/images/icons/overdue_tickets.gif differ diff --git a/scp/images/icons/page.png b/scp/images/icons/page.png new file mode 100644 index 0000000000000000000000000000000000000000..c24bf87795c2b4e5715aa39af8b6cc22a3331d4f Binary files /dev/null and b/scp/images/icons/page.png differ diff --git a/scp/images/icons/phone.png b/scp/images/icons/phone.png new file mode 100644 index 0000000000000000000000000000000000000000..2d9d752918f66982130ab591f2322a807f814965 Binary files /dev/null and b/scp/images/icons/phone.png differ diff --git a/scp/images/icons/premade_reply.gif b/scp/images/icons/premade_reply.gif new file mode 100644 index 0000000000000000000000000000000000000000..524e0e4f5941a594f35c6d70e8852eb4343b0a47 Binary files /dev/null and b/scp/images/icons/premade_reply.gif differ diff --git a/scp/images/icons/printer.gif b/scp/images/icons/printer.gif new file mode 100644 index 0000000000000000000000000000000000000000..c77f40e4736d1d1a52ec1fa65fd6305020453521 Binary files /dev/null and b/scp/images/icons/printer.gif differ diff --git a/scp/images/icons/refresh.gif b/scp/images/icons/refresh.gif new file mode 100644 index 0000000000000000000000000000000000000000..c02a6f47197402a30b4a2b6980a1bd72471caeba Binary files /dev/null and b/scp/images/icons/refresh.gif differ diff --git a/scp/images/icons/settings.gif b/scp/images/icons/settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..261cab9b83d04e7be2ea98343c6c1ce39d15efb0 Binary files /dev/null and b/scp/images/icons/settings.gif differ diff --git a/scp/images/icons/slas.png b/scp/images/icons/slas.png new file mode 100644 index 0000000000000000000000000000000000000000..fd4e4335735b9d82fef4484563ea4197db5b8c69 Binary files /dev/null and b/scp/images/icons/slas.png differ diff --git a/scp/images/icons/teams.gif b/scp/images/icons/teams.gif new file mode 100644 index 0000000000000000000000000000000000000000..691aa495888fe620bf95ab4278427fad1d073d51 Binary files /dev/null and b/scp/images/icons/teams.gif differ diff --git a/scp/images/icons/teams.png b/scp/images/icons/teams.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b84187d065f28798ad9854761951539b000b10 Binary files /dev/null and b/scp/images/icons/teams.png differ diff --git a/scp/images/icons/thread.gif b/scp/images/icons/thread.gif new file mode 100644 index 0000000000000000000000000000000000000000..bffd6b0b3cf5ce0cadfc38683ee7fb3fa0a5c82a Binary files /dev/null and b/scp/images/icons/thread.gif differ diff --git a/scp/images/icons/ticket.gif b/scp/images/icons/ticket.gif new file mode 100644 index 0000000000000000000000000000000000000000..4304ea7955091c46d9fe570faefc643773a5c1de Binary files /dev/null and b/scp/images/icons/ticket.gif differ diff --git a/scp/images/icons/ticket_source_email.gif b/scp/images/icons/ticket_source_email.gif new file mode 100644 index 0000000000000000000000000000000000000000..c551384175a99f5a738ae1ee17a665c9567060f3 Binary files /dev/null and b/scp/images/icons/ticket_source_email.gif differ diff --git a/scp/images/icons/ticket_source_other.gif b/scp/images/icons/ticket_source_other.gif new file mode 100644 index 0000000000000000000000000000000000000000..daa3b8075600aa2fbd12f6434a263a32e00b8294 Binary files /dev/null and b/scp/images/icons/ticket_source_other.gif differ diff --git a/scp/images/icons/ticket_source_phone.gif b/scp/images/icons/ticket_source_phone.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e231f165d6a9c5a6b48f93fe09f5577e97bf5b Binary files /dev/null and b/scp/images/icons/ticket_source_phone.gif differ diff --git a/scp/images/icons/ticket_source_web.gif b/scp/images/icons/ticket_source_web.gif new file mode 100644 index 0000000000000000000000000000000000000000..981b50395c6662a999020fcc9cdc376feab662bd Binary files /dev/null and b/scp/images/icons/ticket_source_web.gif differ diff --git a/scp/images/icons/user_preferences.gif b/scp/images/icons/user_preferences.gif new file mode 100644 index 0000000000000000000000000000000000000000..261cab9b83d04e7be2ea98343c6c1ce39d15efb0 Binary files /dev/null and b/scp/images/icons/user_preferences.gif differ diff --git a/scp/images/nicEditorIcons.gif b/scp/images/nicEditorIcons.gif new file mode 100644 index 0000000000000000000000000000000000000000..5cf1ebedfb7fab0a944c6e8a8e1afe2450153eca Binary files /dev/null and b/scp/images/nicEditorIcons.gif differ diff --git a/scp/images/ost-logo.png b/scp/images/ost-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf40c72dc58b139663d8ba7bd0210ae5d2743f1 Binary files /dev/null and b/scp/images/ost-logo.png differ diff --git a/scp/images/popwin_close.png b/scp/images/popwin_close.png new file mode 100644 index 0000000000000000000000000000000000000000..f3a4df2d44bff2ae902c635199f4b3239ae55e6c Binary files /dev/null and b/scp/images/popwin_close.png differ diff --git a/scp/images/popwin_minimize.png b/scp/images/popwin_minimize.png new file mode 100644 index 0000000000000000000000000000000000000000..06896195b8829b2420f4f8c217ca77e3d277d286 Binary files /dev/null and b/scp/images/popwin_minimize.png differ diff --git a/scp/images/sub-tab.png b/scp/images/sub-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..13b048f7f017e75b964ba1e85611ab34193f26b7 Binary files /dev/null and b/scp/images/sub-tab.png differ diff --git a/scp/images/tab-bg.png b/scp/images/tab-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..16262493293054bc124fd54021655f4a20c63afc Binary files /dev/null and b/scp/images/tab-bg.png differ diff --git a/scp/images/tip_arrow.png b/scp/images/tip_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..4df4e2143212514084421b51ca62e9518e59c1ea Binary files /dev/null and b/scp/images/tip_arrow.png differ diff --git a/scp/index.php b/scp/index.php new file mode 100644 index 0000000000000000000000000000000000000000..dabfde5dc619cad370fbd0fe3be2d0ab4053f331 --- /dev/null +++ b/scp/index.php @@ -0,0 +1,18 @@ +<?php +/********************************************************************* + index.php + + Future site for helpdesk summary aka Dashboard. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +//Nothing for now...simply redirect to tickets page. +require('tickets.php'); +?> diff --git a/scp/js/calendar.js b/scp/js/calendar.js new file mode 100644 index 0000000000000000000000000000000000000000..3a9c87601147e6e97b6990cdf5f23be16ee1e8c1 --- /dev/null +++ b/scp/js/calendar.js @@ -0,0 +1,254 @@ +/* + Tiny DHTML Calendar + + Stolen somewhere online....add credit. + + */ + +function getObj(objID) +{ + if (document.getElementById) {return document.getElementById(objID);} + else if (document.all) {return document.all[objID];} + else if (document.layers) {return document.layers[objID];} +} + +function checkClick(e) { + e?evt=e:evt=event; + CSE=evt.target?evt.target:evt.srcElement; + if (getObj('fc')) + if (!isChild(CSE,getObj('fc'))) + getObj('fc').style.display='none'; +} + +function isChild(s,d) { + while(s) { + if (s==d) + return true; + s=s.parentNode; + } + return false; +} + +function Left(obj) +{ + var curleft = 0; + if (obj.offsetParent) + { + while (obj.offsetParent) + { + curleft += obj.offsetLeft + obj = obj.offsetParent; + } + } + else if (obj.x) + curleft += obj.x; + + + return curleft; +} + +function Top(obj) +{ + var curtop = 0; + if (obj.offsetParent) + { + while (obj.offsetParent) + { + curtop += obj.offsetTop + obj = obj.offsetParent; + } + } + else if (obj.y) + curtop += obj.y; + return curtop; +} + +document.write('<table id="fc" style="position:absolute;border-collapse:collapse;background:#FFFFFF;border:1px solid #ABABAB;display:none" cellpadding=2>'); +document.write('<tr><td style="cursor:pointer" onclick="csubm()"><img src="images/arrowleftmonth.gif"></td><td colspan=5 id="mns" align="center" style="font:bold 13px Arial;text-align:center"></td><td align="right" style="cursor:pointer" onclick="caddm()"><img src="images/arrowrightmonth.gif"></td></tr>'); +document.write('<tr><td align=center style="background:#ABABAB;font:12px Arial">S</td><td align=center style="background:#ABABAB;font:12px Arial">M</td><td align=center style="background:#ABABAB;font:12px Arial">T</td><td align=center style="background:#ABABAB;font:12px Arial">W</td><td align=center style="background:#ABABAB;font:12px Arial">T</td><td align=center style="background:#ABABAB;font:12px Arial">F</td><td align=center style="background:#ABABAB;font:12px Arial">S</td></tr>'); +for(var kk=1;kk<=6;kk++) { + document.write('<tr>'); + for(var tt=1;tt<=7;tt++) { + num=7 * (kk-1) - (-tt); + document.write('<td id="v' + num + '" style="width:18px;height:18px"> </td>'); + } + document.write('</tr>'); +} +document.write('</table>'); + +document.all?document.attachEvent('onclick',checkClick):document.addEventListener('click',checkClick,false); + + +// Calendar script +var now = new Date; +var sccm=now.getMonth(); +var sccy=now.getFullYear(); +var ccm=now.getMonth(); +var ccy=now.getFullYear(); + +var updobj; + +function calendar(ielem) { + + if(ielem) { + ielem.select(); + lcs(ielem); + } +} + + +function lcs(ielem) { + updobj=ielem; + getObj('fc').style.left=Left(ielem)+'px'; + getObj('fc').style.top=Top(ielem)+ielem.offsetHeight+'px'; + getObj('fc').style.display=''; + + // First check date is valid + curdt=ielem.value; + curdtarr=curdt.split('/'); + isdt=true; + for(var k=0;k<curdtarr.length;k++) { + if (isNaN(curdtarr[k])) + isdt=false; + } + if (isdt&(curdtarr.length==3)) { + ccm=curdtarr[0]-1; + ccy=curdtarr[2]; + prepcalendar(curdtarr[1],curdtarr[0]-1,curdtarr[2]); + } + +} + +function evtTgt(e) +{ + var el; + if(e.target)el=e.target; + else if(e.srcElement)el=e.srcElement; + if(el.nodeType==3)el=el.parentNode; // defeat Safari bug + return el; +} +function EvtObj(e){if(!e)e=window.event;return e;} +function cs_over(e) { + evtTgt(EvtObj(e)).style.background='#FFCC66'; +} +function cs_out(e) { + evtTgt(EvtObj(e)).style.background='#C4D3EA'; +} +function cs_click(e) { + updobj.value=calvalarr[evtTgt(EvtObj(e)).id.substring(1,evtTgt(EvtObj(e)).id.length)]; + getObj('fc').style.display='none'; + +} + +var mn=new Array('JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'); +var mnn=new Array('31','28','31','30','31','30','31','31','30','31','30','31'); +var mnl=new Array('31','29','31','30','31','30','31','31','30','31','30','31'); +var calvalarr=new Array(42); + +function f_cps(obj) { + obj.style.background='#C4D3EA'; + obj.style.font='10px Arial'; + obj.style.color='#333333'; + obj.style.textAlign='center'; + obj.style.textDecoration='none'; + obj.style.border='1px solid #6487AE'; + obj.style.cursor='pointer'; +} + +function f_cpps(obj) { + obj.style.background='#C4D3EA'; + obj.style.font='10px Arial'; + obj.style.color='#ABABAB'; + obj.style.textAlign='center'; + obj.style.textDecoration='line-through'; + obj.style.border='1px solid #6487AE'; + obj.style.cursor='default'; +} + +function f_hds(obj) { + obj.style.background='#FFF799'; + obj.style.font='bold 10px Arial'; + obj.style.color='#333333'; + obj.style.textAlign='center'; + obj.style.border='1px solid #6487AE'; + obj.style.cursor='pointer'; +} + +// day selected +function prepcalendar(hd,cm,cy) { + now=new Date(); + sd=now.getDate(); + td=new Date(); + td.setDate(1); + td.setFullYear(cy); + td.setMonth(cm); + cd=td.getDay(); + getObj('mns').innerHTML=mn[cm]+ ' ' + cy; + marr=((cy%4)==0)?mnl:mnn; + for(var d=1;d<=42;d++) { + f_cps(getObj('v'+parseInt(d))); + if ((d >= (cd -(-1))) && (d<=cd-(-marr[cm]))) { + dip=((d-cd < sd)&&(cm==sccm)&&(cy==sccy)); + htd=((hd!='')&&(d-cd==hd)); + if (0 && dip) + f_cpps(getObj('v'+parseInt(d))); + else if (htd) + f_hds(getObj('v'+parseInt(d))); + else + f_cps(getObj('v'+parseInt(d))); + + getObj('v'+parseInt(d)).onmouseover=cs_over; + getObj('v'+parseInt(d)).onmouseout=cs_out; + getObj('v'+parseInt(d)).onclick=cs_click; + + getObj('v'+parseInt(d)).innerHTML=d-cd; + calvalarr[d]=''+(cm-(-1))+'/'+(d-cd)+'/'+cy; + } + else { + getObj('v'+d).innerHTML=' '; + getObj('v'+parseInt(d)).onmouseover=null; + getObj('v'+parseInt(d)).onmouseout=null; + getObj('v'+parseInt(d)).style.cursor='default'; + } + } +} + +prepcalendar('',ccm,ccy); +//getObj('fc'+cc).style.visibility='hidden'; + +function caddm() { + marr=((ccy%4)==0)?mnl:mnn; + + ccm+=1; + if (ccm>=12) { + ccm=0; + ccy++; + } + cdayf(); + prepcalendar('',ccm,ccy); +} + +function csubm() { + marr=((ccy%4)==0)?mnl:mnn; + + ccm-=1; + if (ccm<0) { + ccm=11; + ccy--; + } + cdayf(); + prepcalendar('',ccm,ccy); +} + +function cdayf() { + + return; + + if ((ccy>sccy)|((ccy==sccy)&&(ccm>=sccm))) + return; + else { + ccy=sccy; + ccm=sccm; + cfd=scfd; + } +} diff --git a/scp/js/nicEdit.js b/scp/js/nicEdit.js new file mode 100644 index 0000000000000000000000000000000000000000..86cdffd44350a44df70090f4b472524cc2d152aa --- /dev/null +++ b/scp/js/nicEdit.js @@ -0,0 +1,102 @@ +/* NicEdit - Micro Inline WYSIWYG + * Copyright 2007-2008 Brian Kirchoff + * + * NicEdit is distributed under the terms of the MIT license + * For more information visit http://nicedit.com/ + * Do not remove this copyright message + */ +var bkExtend=function(){var A=arguments;if(A.length==1){A=[this,A[0]]}for(var B in A[1]){A[0][B]=A[1][B]}return A[0]};function bkClass(){}bkClass.prototype.construct=function(){};bkClass.extend=function(C){var A=function(){if(arguments[0]!==bkClass){return this.construct.apply(this,arguments)}};var B=new this(bkClass);bkExtend(B,C);A.prototype=B;A.extend=this.extend;return A};var bkElement=bkClass.extend({construct:function(B,A){if(typeof (B)=="string"){B=(A||document).createElement(B)}B=$BK(B);return B},appendTo:function(A){A.appendChild(this);return this},appendBefore:function(A){A.parentNode.insertBefore(this,A);return this},addEvent:function(B,A){bkLib.addEvent(this,B,A);return this},setContent:function(A){this.innerHTML=A;return this},pos:function(){var C=curtop=0;var B=obj=this;if(obj.offsetParent){do{C+=obj.offsetLeft;curtop+=obj.offsetTop}while(obj=obj.offsetParent)}var A=(!window.opera)?parseInt(this.getStyle("border-width")||this.style.border)||0:0;return[C+A,curtop+A+this.offsetHeight]},noSelect:function(){bkLib.noSelect(this);return this},parentTag:function(A){var B=this;do{if(B&&B.nodeName&&B.nodeName.toUpperCase()==A){return B}B=B.parentNode}while(B);return false},hasClass:function(A){return this.className.match(new RegExp("(\\s|^)nicEdit-"+A+"(\\s|$)"))},addClass:function(A){if(!this.hasClass(A)){this.className+=" nicEdit-"+A}return this},removeClass:function(A){if(this.hasClass(A)){this.className=this.className.replace(new RegExp("(\\s|^)nicEdit-"+A+"(\\s|$)")," ")}return this},setStyle:function(A){var B=this.style;for(var C in A){switch(C){case"float":B.cssFloat=B.styleFloat=A[C];break;case"opacity":B.opacity=A[C];B.filter="alpha(opacity="+Math.round(A[C]*100)+")";break;case"className":this.className=A[C];break;default:B[C]=A[C]}}return this},getStyle:function(A,C){var B=(!C)?document.defaultView:C;if(this.nodeType==1){return(B&&B.getComputedStyle)?B.getComputedStyle(this,null).getPropertyValue(A):this.currentStyle[bkLib.camelize(A)]}},remove:function(){this.parentNode.removeChild(this);return this},setAttributes:function(A){for(var B in A){this[B]=A[B]}return this}});var bkLib={isMSIE:(navigator.appVersion.indexOf("MSIE")!=-1),addEvent:function(C,B,A){(C.addEventListener)?C.addEventListener(B,A,false):C.attachEvent("on"+B,A)},toArray:function(C){var B=C.length,A=new Array(B);while(B--){A[B]=C[B]}return A},noSelect:function(B){if(B.setAttribute&&B.nodeName.toLowerCase()!="input"&&B.nodeName.toLowerCase()!="textarea"){B.setAttribute("unselectable","on")}for(var A=0;A<B.childNodes.length;A++){bkLib.noSelect(B.childNodes[A])}},camelize:function(A){return A.replace(/\-(.)/g,function(B,C){return C.toUpperCase()})},inArray:function(A,B){return(bkLib.search(A,B)!=null)},search:function(A,C){for(var B=0;B<A.length;B++){if(A[B]==C){return B}}return null},cancelEvent:function(A){A=A||window.event;if(A.preventDefault&&A.stopPropagation){A.preventDefault();A.stopPropagation()}return false},domLoad:[],domLoaded:function(){if(arguments.callee.done){return }arguments.callee.done=true;for(i=0;i<bkLib.domLoad.length;i++){bkLib.domLoad[i]()}},onDomLoaded:function(A){this.domLoad.push(A);if(document.addEventListener){document.addEventListener("DOMContentLoaded",bkLib.domLoaded,null)}else{if(bkLib.isMSIE){document.write("<style>.nicEdit-main p { margin: 0; }</style><script id=__ie_onload defer "+((location.protocol=="https:")?"src='javascript:void(0)'":"src=//0")+"><\/script>");$BK("__ie_onload").onreadystatechange=function(){if(this.readyState=="complete"){bkLib.domLoaded()}}}}window.onload=bkLib.domLoaded}};function $BK(A){if(typeof (A)=="string"){A=document.getElementById(A)}return(A&&!A.appendTo)?bkExtend(A,bkElement.prototype):A}var bkEvent={addEvent:function(A,B){if(B){this.eventList=this.eventList||{};this.eventList[A]=this.eventList[A]||[];this.eventList[A].push(B)}return this},fireEvent:function(){var A=bkLib.toArray(arguments),C=A.shift();if(this.eventList&&this.eventList[C]){for(var B=0;B<this.eventList[C].length;B++){this.eventList[C][B].apply(this,A)}}}};function __(A){return A}Function.prototype.closure=function(){var A=this,B=bkLib.toArray(arguments),C=B.shift();return function(){if(typeof (bkLib)!="undefined"){return A.apply(C,B.concat(bkLib.toArray(arguments)))}}};Function.prototype.closureListener=function(){var A=this,C=bkLib.toArray(arguments),B=C.shift();return function(E){E=E||window.event;if(E.target){var D=E.target}else{var D=E.srcElement}return A.apply(B,[E,D].concat(C))}}; + + + +var nicEditorConfig = bkClass.extend({ + buttons : { + 'bold' : {name : __('Click to Bold'), command : 'Bold', tags : ['B','STRONG'], css : {'font-weight' : 'bold'}, key : 'b'}, + 'italic' : {name : __('Click to Italic'), command : 'Italic', tags : ['EM','I'], css : {'font-style' : 'italic'}, key : 'i'}, + 'underline' : {name : __('Click to Underline'), command : 'Underline', tags : ['U'], css : {'text-decoration' : 'underline'}, key : 'u'}, + 'left' : {name : __('Left Align'), command : 'justifyleft', noActive : true}, + 'center' : {name : __('Center Align'), command : 'justifycenter', noActive : true}, + 'right' : {name : __('Right Align'), command : 'justifyright', noActive : true}, + 'justify' : {name : __('Justify Align'), command : 'justifyfull', noActive : true}, + 'ol' : {name : __('Insert Ordered List'), command : 'insertorderedlist', tags : ['OL']}, + 'ul' : {name : __('Insert Unordered List'), command : 'insertunorderedlist', tags : ['UL']}, + 'subscript' : {name : __('Click to Subscript'), command : 'subscript', tags : ['SUB']}, + 'superscript' : {name : __('Click to Superscript'), command : 'superscript', tags : ['SUP']}, + 'strikethrough' : {name : __('Click to Strike Through'), command : 'strikeThrough', css : {'text-decoration' : 'line-through'}}, + 'removeformat' : {name : __('Remove Formatting'), command : 'removeformat', noActive : true}, + 'indent' : {name : __('Indent Text'), command : 'indent', noActive : true}, + 'outdent' : {name : __('Remove Indent'), command : 'outdent', noActive : true}, + 'hr' : {name : __('Horizontal Rule'), command : 'insertHorizontalRule', noActive : true} + }, + iconsPath : '../nicEditorIcons.gif', + buttonList : ['save','bold','italic','underline','left','center','right','justify','ol','ul','fontSize','fontFamily','fontFormat','indent','outdent','image','upload','link','unlink','forecolor','bgcolor'], + iconList : {"bgcolor":1,"forecolor":2,"bold":3,"center":4,"hr":5,"indent":6,"italic":7,"justify":8,"left":9,"ol":10,"outdent":11,"removeformat":12,"right":13,"save":24,"strikethrough":15,"subscript":16,"superscript":17,"ul":18,"underline":19,"image":20,"link":21,"unlink":22,"close":23,"arrow":25} + +}); +; +var nicEditors={nicPlugins:[],editors:[],registerPlugin:function(B,A){this.nicPlugins.push({p:B,o:A})},allTextAreas:function(C){var A=document.getElementsByTagName("textarea");for(var B=0;B<A.length;B++){nicEditors.editors.push(new nicEditor(C).panelInstance(A[B]))}return nicEditors.editors},findEditor:function(C){var B=nicEditors.editors;for(var A=0;A<B.length;A++){if(B[A].instanceById(C)){return B[A].instanceById(C)}}}};var nicEditor=bkClass.extend({construct:function(C){this.options=new nicEditorConfig();bkExtend(this.options,C);this.nicInstances=new Array();this.loadedPlugins=new Array();var A=nicEditors.nicPlugins;for(var B=0;B<A.length;B++){this.loadedPlugins.push(new A[B].p(this,A[B].o))}nicEditors.editors.push(this);bkLib.addEvent(document.body,"mousedown",this.selectCheck.closureListener(this))},panelInstance:function(B,C){B=this.checkReplace($BK(B));var A=new bkElement("DIV").setStyle({width:(B.clientWidth || parseInt(B.getStyle("width")))+"px"}).appendBefore(B);this.setPanel(A);return this.addInstance(B,C)},checkReplace:function(B){var A=nicEditors.findEditor(B);if(A){A.removeInstance(B);A.removePanel()}return B},addInstance:function(B,C){B=this.checkReplace($BK(B));if(B.contentEditable||!!window.opera){var A=new nicEditorInstance(B,C,this)}else{var A=new nicEditorIFrameInstance(B,C,this)}this.nicInstances.push(A);return this},removeInstance:function(C){C=$BK(C);var B=this.nicInstances;for(var A=0;A<B.length;A++){if(B[A].e==C){B[A].remove();this.nicInstances.splice(A,1)}}},removePanel:function(A){if(this.nicPanel){this.nicPanel.remove();this.nicPanel=null}},instanceById:function(C){C=$BK(C);var B=this.nicInstances;for(var A=0;A<B.length;A++){if(B[A].e==C){return B[A]}}},setPanel:function(A){this.nicPanel=new nicEditorPanel($BK(A),this.options,this);this.fireEvent("panel",this.nicPanel);return this},nicCommand:function(B,A){if(this.selectedInstance){this.selectedInstance.nicCommand(B,A)}},getIcon:function(D,A){var C=this.options.iconList[D];var B=(A.iconFiles)?A.iconFiles[D]:"";return{backgroundImage:"url('"+((C)?this.options.iconsPath:B)+"')",backgroundPosition:((C)?((C-1)*-18):0)+"px 0px"}},selectCheck:function(C,A){var B=false;do{if(A.className&&A.className.indexOf("nicEdit")!=-1){return false}}while(A=A.parentNode);this.fireEvent("blur",this.selectedInstance,A);this.lastSelectedInstance=this.selectedInstance;this.selectedInstance=null;return false}});nicEditor=nicEditor.extend(bkEvent); +var nicEditorInstance=bkClass.extend({isSelected:false,construct:function(G,D,C){this.ne=C;this.elm=this.e=G;this.options=D||{};newX=parseInt(G.clientWidth || G.getStyle("width"));newY=parseInt(G.getStyle("height"))||G.clientHeight;this.initialHeight=newY-8;var H=(G.nodeName.toLowerCase()=="textarea");if(H||this.options.hasPanel){var B=(bkLib.isMSIE&&!((typeof document.body.style.maxHeight!="undefined")&&document.compatMode=="CSS1Compat"));var E={width:newX+"px",border:"1px solid #ccc",borderTop:0,overflowY:"auto",overflowX:"hidden"};E[(B)?"height":"maxHeight"]=(this.ne.options.maxHeight)?this.ne.options.maxHeight+"px":null;this.editorContain=new bkElement("DIV").setStyle(E).appendBefore(G);var A=new bkElement("DIV").setStyle({width:(newX-8)+"px",margin:"4px",minHeight:newY+"px"}).addClass("main").appendTo(this.editorContain);G.setStyle({display:"none"});A.innerHTML=G.innerHTML;if(H){A.setContent(G.value);this.copyElm=G;var F=G.parentTag("FORM");if(F){bkLib.addEvent(F,"submit",this.saveContent.closure(this))}}A.setStyle((B)?{height:newY+"px"}:{overflow:"hidden"});this.elm=A}this.ne.addEvent("blur",this.blur.closure(this));this.init();this.blur()},init:function(){this.elm.setAttribute("contentEditable","true");if(this.getContent()==""){this.setContent("")}this.instanceDoc=document.defaultView;this.elm.addEvent("mousedown",this.selected.closureListener(this)).addEvent("keypress",this.keyDown.closureListener(this)).addEvent("focus",this.selected.closure(this)).addEvent("blur",this.blur.closure(this)).addEvent("keyup",this.selected.closure(this));this.ne.fireEvent("add",this)},remove:function(){this.saveContent();if(this.copyElm||this.options.hasPanel){this.editorContain.remove();this.e.setStyle({display:"block"});this.ne.removePanel()}this.disable();this.ne.fireEvent("remove",this)},disable:function(){this.elm.setAttribute("contentEditable","false")},getSel:function(){return(window.getSelection)?window.getSelection():document.selection},getRng:function(){var A=this.getSel();if(!A){return null}return(A.rangeCount>0)?A.getRangeAt(0):A.createRange()},selRng:function(A,B){if(window.getSelection){B.removeAllRanges();B.addRange(A)}else{A.select()}},selElm:function(){var C=this.getRng();if(C.startContainer){var D=C.startContainer;if(C.cloneContents().childNodes.length==1){for(var B=0;B<D.childNodes.length;B++){var A=D.childNodes[B].ownerDocument.createRange();A.selectNode(D.childNodes[B]);if(C.compareBoundaryPoints(Range.START_TO_START,A)!=1&&C.compareBoundaryPoints(Range.END_TO_END,A)!=-1){return $BK(D.childNodes[B])}}}return $BK(D)}else{return $BK((this.getSel().type=="Control")?C.item(0):C.parentElement())}},saveRng:function(){this.savedRange=this.getRng();this.savedSel=this.getSel()},restoreRng:function(){if(this.savedRange){this.selRng(this.savedRange,this.savedSel)}},keyDown:function(B,A){if(B.ctrlKey){this.ne.fireEvent("key",this,B)}},selected:function(C,A){if(!A){A=this.selElm()}if(!C.ctrlKey){var B=this.ne.selectedInstance;if(B!=this){if(B){this.ne.fireEvent("blur",B,A)}this.ne.selectedInstance=this;this.ne.fireEvent("focus",B,A)}this.ne.fireEvent("selected",B,A);this.isFocused=true;this.elm.addClass("selected")}return false},blur:function(){this.isFocused=false;this.elm.removeClass("selected")},saveContent:function(){if(this.copyElm||this.options.hasPanel){this.ne.fireEvent("save",this);(this.copyElm)?this.copyElm.value=this.getContent():this.e.innerHTML=this.getContent()}},getElm:function(){return this.elm},getContent:function(){this.content=this.getElm().innerHTML;this.ne.fireEvent("get",this);return this.content},setContent:function(A){this.content=A;this.ne.fireEvent("set",this);this.elm.innerHTML=this.content},nicCommand:function(B,A){document.execCommand(B,false,A)}}); +var nicEditorIFrameInstance=nicEditorInstance.extend({savedStyles:[],init:function(){var B=this.elm.innerHTML.replace(/^\s+|\s+$/g,"");this.elm.innerHTML="";(!B)?B="":B;this.initialContent=B;this.elmFrame=new bkElement("iframe").setAttributes({src:"javascript:;",frameBorder:0,allowTransparency:"true",scrolling:"no"}).setStyle({height:"100px",width:"100%"}).addClass("frame").appendTo(this.elm);if(this.copyElm){this.elmFrame.setStyle({width:(this.elm.offsetWidth-4)+"px"})}var A=["font-size","font-family","font-weight","color"];for(itm in A){this.savedStyles[bkLib.camelize(itm)]=this.elm.getStyle(itm)}setTimeout(this.initFrame.closure(this),50)},disable:function(){this.elm.innerHTML=this.getContent()},initFrame:function(){var B=$BK(this.elmFrame.contentWindow.document);B.designMode="on";B.open();var A=this.ne.options.externalCSS;B.write("<html><head>"+((A)?'<link href="'+A+'" rel="stylesheet" type="text/css" />':"")+'</head><body id="nicEditContent" style="margin: 0 !important; background-color: transparent !important;">'+this.initialContent+"</body></html>");B.close();this.frameDoc=B;this.frameWin=$BK(this.elmFrame.contentWindow);this.frameContent=$BK(this.frameWin.document.body).setStyle(this.savedStyles);this.instanceDoc=this.frameWin.document.defaultView;this.heightUpdate();this.frameDoc.addEvent("mousedown",this.selected.closureListener(this)).addEvent("keyup",this.heightUpdate.closureListener(this)).addEvent("keydown",this.keyDown.closureListener(this)).addEvent("keyup",this.selected.closure(this));this.ne.fireEvent("add",this)},getElm:function(){return this.frameContent},setContent:function(A){this.content=A;this.ne.fireEvent("set",this);this.frameContent.innerHTML=this.content;this.heightUpdate()},getSel:function(){return(this.frameWin)?this.frameWin.getSelection():this.frameDoc.selection},heightUpdate:function(){this.elmFrame.style.height=Math.max(this.frameContent.offsetHeight,this.initialHeight)+"px"},nicCommand:function(B,A){this.frameDoc.execCommand(B,false,A);setTimeout(this.heightUpdate.closure(this),100)}}); +var nicEditorPanel=bkClass.extend({construct:function(E,B,A){this.elm=E;this.options=B;this.ne=A;this.panelButtons=new Array();this.buttonList=bkExtend([],this.ne.options.buttonList);this.panelContain=new bkElement("DIV").setStyle({overflow:"hidden",width:"100%",border:"1px solid #cccccc",backgroundColor:"#efefef"}).addClass("panelContain");this.panelElm=new bkElement("DIV").setStyle({margin:"2px",marginTop:"0px",zoom:1,overflow:"hidden"}).addClass("panel").appendTo(this.panelContain);this.panelContain.appendTo(E);var C=this.ne.options;var D=C.buttons;for(button in D){this.addButton(button,C,true)}this.reorder();E.noSelect()},addButton:function(buttonName,options,noOrder){var button=options.buttons[buttonName];var type=(button.type)?eval("(typeof("+button.type+') == "undefined") ? null : '+button.type+";"):nicEditorButton;var hasButton=bkLib.inArray(this.buttonList,buttonName);if(type&&(hasButton||this.ne.options.fullPanel)){this.panelButtons.push(new type(this.panelElm,buttonName,options,this.ne));if(!hasButton){this.buttonList.push(buttonName)}}},findButton:function(B){for(var A=0;A<this.panelButtons.length;A++){if(this.panelButtons[A].name==B){return this.panelButtons[A]}}},reorder:function(){var C=this.buttonList;for(var B=0;B<C.length;B++){var A=this.findButton(C[B]);if(A){this.panelElm.appendChild(A.margin)}}},remove:function(){this.elm.remove()}}); +var nicEditorButton=bkClass.extend({construct:function(D,A,C,B){this.options=C.buttons[A];this.name=A;this.ne=B;this.elm=D;this.margin=new bkElement("DIV").setStyle({"float":"left",marginTop:"2px"}).appendTo(D);this.contain=new bkElement("DIV").setStyle({width:"20px",height:"20px"}).addClass("buttonContain").appendTo(this.margin);this.border=new bkElement("DIV").setStyle({backgroundColor:"#efefef",border:"1px solid #efefef"}).appendTo(this.contain);this.button=new bkElement("DIV").setStyle({width:"18px",height:"18px",overflow:"hidden",zoom:1,cursor:"pointer"}).addClass("button").setStyle(this.ne.getIcon(A,C)).appendTo(this.border);this.button.addEvent("mouseover",this.hoverOn.closure(this)).addEvent("mouseout",this.hoverOff.closure(this)).addEvent("mousedown",this.mouseClick.closure(this)).noSelect();if(!window.opera){this.button.onmousedown=this.button.onclick=bkLib.cancelEvent}B.addEvent("selected",this.enable.closure(this)).addEvent("blur",this.disable.closure(this)).addEvent("key",this.key.closure(this));this.disable();this.init()},init:function(){},hide:function(){this.contain.setStyle({display:"none"})},updateState:function(){if(this.isDisabled){this.setBg()}else{if(this.isHover){this.setBg("hover")}else{if(this.isActive){this.setBg("active")}else{this.setBg()}}}},setBg:function(A){switch(A){case"hover":var B={border:"1px solid #666",backgroundColor:"#ddd"};break;case"active":var B={border:"1px solid #666",backgroundColor:"#ccc"};break;default:var B={border:"1px solid #efefef",backgroundColor:"#efefef"}}this.border.setStyle(B).addClass("button-"+A)},checkNodes:function(A){var B=A;do{if(this.options.tags&&bkLib.inArray(this.options.tags,B.nodeName)){this.activate();return true}}while(B=B.parentNode&&B.className!="nicEdit");B=$BK(A);while(B.nodeType==3){B=$BK(B.parentNode)}if(this.options.css){for(itm in this.options.css){if(B.getStyle(itm,this.ne.selectedInstance.instanceDoc)==this.options.css[itm]){this.activate();return true}}}this.deactivate();return false},activate:function(){if(!this.isDisabled){this.isActive=true;this.updateState();this.ne.fireEvent("buttonActivate",this)}},deactivate:function(){this.isActive=false;this.updateState();if(!this.isDisabled){this.ne.fireEvent("buttonDeactivate",this)}},enable:function(A,B){this.isDisabled=false;this.contain.setStyle({opacity:1}).addClass("buttonEnabled");this.updateState();this.checkNodes(B)},disable:function(A,B){this.isDisabled=true;this.contain.setStyle({opacity:0.6}).removeClass("buttonEnabled");this.updateState()},toggleActive:function(){(this.isActive)?this.deactivate():this.activate()},hoverOn:function(){if(!this.isDisabled){this.isHover=true;this.updateState();this.ne.fireEvent("buttonOver",this)}},hoverOff:function(){this.isHover=false;this.updateState();this.ne.fireEvent("buttonOut",this)},mouseClick:function(){if(this.options.command){this.ne.nicCommand(this.options.command,this.options.commandArgs);if(!this.options.noActive){this.toggleActive()}}this.ne.fireEvent("buttonClick",this)},key:function(A,B){if(this.options.key&&B.ctrlKey&&String.fromCharCode(B.keyCode||B.charCode).toLowerCase()==this.options.key){this.mouseClick();if(B.preventDefault){B.preventDefault()}}}}); +var nicPlugin=bkClass.extend({construct:function(B,A){this.options=A;this.ne=B;this.ne.addEvent("panel",this.loadPanel.closure(this));this.init()},loadPanel:function(C){var B=this.options.buttons;for(var A in B){C.addButton(A,this.options)}C.reorder()},init:function(){}}); + + +var nicPaneOptions = { }; + +var nicEditorPane=bkClass.extend({construct:function(D,C,B,A){this.ne=C;this.elm=D;this.pos=D.pos();this.contain=new bkElement("div").setStyle({zIndex:"99999",overflow:"hidden",position:"absolute",left:this.pos[0]+"px",top:this.pos[1]+"px"});this.pane=new bkElement("div").setStyle({fontSize:"12px",border:"1px solid #ccc",overflow:"hidden",padding:"4px",textAlign:"left",backgroundColor:"#ffffc9"}).addClass("pane").setStyle(B).appendTo(this.contain);if(A&&!A.options.noClose){this.close=new bkElement("div").setStyle({"float":"right",height:"16px",width:"16px",cursor:"pointer"}).setStyle(this.ne.getIcon("close",nicPaneOptions)).addEvent("mousedown",A.removePane.closure(this)).appendTo(this.pane)}this.contain.noSelect().appendTo(document.body);this.position();this.init()},init:function(){},position:function(){if(this.ne.nicPanel){var B=this.ne.nicPanel.elm;var A=B.pos();var C=A[0]+parseInt(B.getStyle("width"))-(parseInt(this.pane.getStyle("width"))+8);if(C<this.pos[0]){this.contain.setStyle({left:C+"px"})}}},toggle:function(){this.isVisible=!this.isVisible;this.contain.setStyle({display:((this.isVisible)?"block":"none")})},remove:function(){if(this.contain){this.contain.remove();this.contain=null}},append:function(A){A.appendTo(this.pane)},setContent:function(A){this.pane.setContent(A)}}); + +var nicEditorAdvancedButton=nicEditorButton.extend({init:function(){this.ne.addEvent("selected",this.removePane.closure(this)).addEvent("blur",this.removePane.closure(this))},mouseClick:function(){if(!this.isDisabled){if(this.pane&&this.pane.pane){this.removePane()}else{this.pane=new nicEditorPane(this.contain,this.ne,{width:(this.width||"270px"),backgroundColor:"#fff"},this);this.addPane();this.ne.selectedInstance.saveRng()}}},addForm:function(C,G){this.form=new bkElement("form").addEvent("submit",this.submit.closureListener(this));this.pane.append(this.form);this.inputs={};for(itm in C){var D=C[itm];var F="";if(G){F=G.getAttribute(itm)}if(!F){F=D.value||""}var A=C[itm].type;if(A=="title"){new bkElement("div").setContent(D.txt).setStyle({fontSize:"14px",fontWeight:"bold",padding:"0px",margin:"2px 0"}).appendTo(this.form)}else{var B=new bkElement("div").setStyle({overflow:"hidden",clear:"both"}).appendTo(this.form);if(D.txt){new bkElement("label").setAttributes({"for":itm}).setContent(D.txt).setStyle({margin:"2px 4px",fontSize:"13px",width:"50px",lineHeight:"20px",textAlign:"right","float":"left"}).appendTo(B)}switch(A){case"text":this.inputs[itm]=new bkElement("input").setAttributes({id:itm,value:F,type:"text"}).setStyle({margin:"2px 0",fontSize:"13px","float":"left",height:"20px",border:"1px solid #ccc",overflow:"hidden"}).setStyle(D.style).appendTo(B);break;case"select":this.inputs[itm]=new bkElement("select").setAttributes({id:itm}).setStyle({border:"1px solid #ccc","float":"left",margin:"2px 0"}).appendTo(B);for(opt in D.options){var E=new bkElement("option").setAttributes({value:opt,selected:(opt==F)?"selected":""}).setContent(D.options[opt]).appendTo(this.inputs[itm])}break;case"content":this.inputs[itm]=new bkElement("textarea").setAttributes({id:itm}).setStyle({border:"1px solid #ccc","float":"left"}).setStyle(D.style).appendTo(B);this.inputs[itm].value=F}}}new bkElement("input").setAttributes({type:"submit"}).setStyle({backgroundColor:"#efefef",border:"1px solid #ccc",margin:"3px 0","float":"left",clear:"both"}).appendTo(this.form);this.form.onsubmit=bkLib.cancelEvent},submit:function(){},findElm:function(B,A,E){var D=this.ne.selectedInstance.getElm().getElementsByTagName(B);for(var C=0;C<D.length;C++){if(D[C].getAttribute(A)==E){return $BK(D[C])}}},removePane:function(){if(this.pane){this.pane.remove();this.pane=null;this.ne.selectedInstance.restoreRng()}}}); + +var nicButtonTips=bkClass.extend({construct:function(A){this.ne=A;A.addEvent("buttonOver",this.show.closure(this)).addEvent("buttonOut",this.hide.closure(this))},show:function(A){this.timer=setTimeout(this.create.closure(this,A),400)},create:function(A){this.timer=null;if(!this.pane){this.pane=new nicEditorPane(A.button,this.ne,{fontSize:"12px",marginTop:"5px"});this.pane.setContent(A.options.name)}},hide:function(A){if(this.timer){clearTimeout(this.timer)}if(this.pane){this.pane=this.pane.remove()}}});nicEditors.registerPlugin(nicButtonTips); + + +var nicSelectOptions = { + buttons : { + 'fontSize' : {name : __('Select Font Size'), type : 'nicEditorFontSizeSelect', command : 'fontsize'}, + 'fontFamily' : {name : __('Select Font Family'), type : 'nicEditorFontFamilySelect', command : 'fontname'}, + 'fontFormat' : {name : __('Select Font Format'), type : 'nicEditorFontFormatSelect', command : 'formatBlock'} + } +}; + +var nicEditorSelect=bkClass.extend({construct:function(D,A,C,B){this.options=C.buttons[A];this.elm=D;this.ne=B;this.name=A;this.selOptions=new Array();this.margin=new bkElement("div").setStyle({"float":"left",margin:"2px 1px 0 1px"}).appendTo(this.elm);this.contain=new bkElement("div").setStyle({width:"90px",height:"20px",cursor:"pointer",overflow:"hidden"}).addClass("selectContain").addEvent("click",this.toggle.closure(this)).appendTo(this.margin);this.items=new bkElement("div").setStyle({overflow:"hidden",zoom:1,border:"1px solid #ccc",paddingLeft:"3px",backgroundColor:"#fff"}).appendTo(this.contain);this.control=new bkElement("div").setStyle({overflow:"hidden","float":"right",height:"18px",width:"16px"}).addClass("selectControl").setStyle(this.ne.getIcon("arrow",C)).appendTo(this.items);this.txt=new bkElement("div").setStyle({overflow:"hidden","float":"left",width:"66px",height:"14px",marginTop:"1px",fontFamily:"sans-serif",textAlign:"center",fontSize:"12px"}).addClass("selectTxt").appendTo(this.items);if(!window.opera){this.contain.onmousedown=this.control.onmousedown=this.txt.onmousedown=bkLib.cancelEvent}this.margin.noSelect();this.ne.addEvent("selected",this.enable.closure(this)).addEvent("blur",this.disable.closure(this));this.disable();this.init()},disable:function(){this.isDisabled=true;this.close();this.contain.setStyle({opacity:0.6})},enable:function(A){this.isDisabled=false;this.close();this.contain.setStyle({opacity:1})},setDisplay:function(A){this.txt.setContent(A)},toggle:function(){if(!this.isDisabled){(this.pane)?this.close():this.open()}},open:function(){this.pane=new nicEditorPane(this.items,this.ne,{width:"88px",padding:"0px",borderTop:0,borderLeft:"1px solid #ccc",borderRight:"1px solid #ccc",borderBottom:"0px",backgroundColor:"#fff"});for(var C=0;C<this.selOptions.length;C++){var B=this.selOptions[C];var A=new bkElement("div").setStyle({overflow:"hidden",borderBottom:"1px solid #ccc",width:"88px",textAlign:"left",overflow:"hidden",cursor:"pointer"});var D=new bkElement("div").setStyle({padding:"0px 4px"}).setContent(B[1]).appendTo(A).noSelect();D.addEvent("click",this.update.closure(this,B[0])).addEvent("mouseover",this.over.closure(this,D)).addEvent("mouseout",this.out.closure(this,D)).setAttributes("id",B[0]);this.pane.append(A);if(!window.opera){D.onmousedown=bkLib.cancelEvent}}},close:function(){if(this.pane){this.pane=this.pane.remove()}},over:function(A){A.setStyle({backgroundColor:"#ccc"})},out:function(A){A.setStyle({backgroundColor:"#fff"})},add:function(B,A){this.selOptions.push(new Array(B,A))},update:function(A){this.ne.nicCommand(this.options.command,A);this.close()}});var nicEditorFontSizeSelect=nicEditorSelect.extend({sel:{1:"1 (8pt)",2:"2 (10pt)",3:"3 (12pt)",4:"4 (14pt)",5:"5 (18pt)",6:"6 (24pt)"},init:function(){this.setDisplay("Font Size...");for(itm in this.sel){this.add(itm,'<font size="'+itm+'">'+this.sel[itm]+"</font>")}}});var nicEditorFontFamilySelect=nicEditorSelect.extend({sel:{arial:"Arial","comic sans ms":"Comic Sans","courier new":"Courier New",georgia:"Georgia",helvetica:"Helvetica",impact:"Impact","times new roman":"Times","trebuchet ms":"Trebuchet",verdana:"Verdana"},init:function(){this.setDisplay("Font Family...");for(itm in this.sel){this.add(itm,'<font face="'+itm+'">'+this.sel[itm]+"</font>")}}});var nicEditorFontFormatSelect=nicEditorSelect.extend({sel:{p:"Paragraph",pre:"Pre",h6:"Heading 6",h5:"Heading 5",h4:"Heading 4",h3:"Heading 3",h2:"Heading 2",h1:"Heading 1"},init:function(){this.setDisplay("Font Format...");for(itm in this.sel){var A=itm.toUpperCase();this.add("<"+A+">","<"+itm+' style="padding: 0px; margin: 0px;">'+this.sel[itm]+"</"+A+">")}}});nicEditors.registerPlugin(nicPlugin,nicSelectOptions); + + +var nicLinkOptions = { + buttons : { + 'link' : {name : 'Add Link', type : 'nicLinkButton', tags : ['A']}, + 'unlink' : {name : 'Remove Link', command : 'unlink', noActive : true} + } +}; + +var nicLinkButton=nicEditorAdvancedButton.extend({addPane:function(){this.ln=this.ne.selectedInstance.selElm().parentTag("A");this.addForm({"":{type:"title",txt:"Add/Edit Link"},href:{type:"text",txt:"URL",value:"http://",style:{width:"150px"}},title:{type:"text",txt:"Title"},target:{type:"select",txt:"Open In",options:{"":"Current Window",_blank:"New Window"},style:{width:"100px"}}},this.ln)},submit:function(C){var A=this.inputs.href.value;if(A=="http://"||A==""){alert("You must enter a URL to Create a Link");return false}this.removePane();if(!this.ln){var B="javascript:nicTemp();";this.ne.nicCommand("createlink",B);this.ln=this.findElm("A","href",B)}if(this.ln){this.ln.setAttributes({href:this.inputs.href.value,title:this.inputs.title.value,target:this.inputs.target.options[this.inputs.target.selectedIndex].value})}}});nicEditors.registerPlugin(nicPlugin,nicLinkOptions); + + +var nicColorOptions = { + buttons : { + 'forecolor' : {name : __('Change Text Color'), type : 'nicEditorColorButton', noClose : true}, + 'bgcolor' : {name : __('Change Background Color'), type : 'nicEditorBgColorButton', noClose : true} + } +}; + +var nicEditorColorButton=nicEditorAdvancedButton.extend({addPane:function(){var D={0:"00",1:"33",2:"66",3:"99",4:"CC",5:"FF"};var H=new bkElement("DIV").setStyle({width:"270px"});for(var A in D){for(var F in D){for(var E in D){var I="#"+D[A]+D[E]+D[F];var C=new bkElement("DIV").setStyle({cursor:"pointer",height:"15px","float":"left"}).appendTo(H);var G=new bkElement("DIV").setStyle({border:"2px solid "+I}).appendTo(C);var B=new bkElement("DIV").setStyle({backgroundColor:I,overflow:"hidden",width:"11px",height:"11px"}).addEvent("click",this.colorSelect.closure(this,I)).addEvent("mouseover",this.on.closure(this,G)).addEvent("mouseout",this.off.closure(this,G,I)).appendTo(G);if(!window.opera){C.onmousedown=B.onmousedown=bkLib.cancelEvent}}}}this.pane.append(H.noSelect())},colorSelect:function(A){this.ne.nicCommand("foreColor",A);this.removePane()},on:function(A){A.setStyle({border:"2px solid #000"})},off:function(A,B){A.setStyle({border:"2px solid "+B})}});var nicEditorBgColorButton=nicEditorColorButton.extend({colorSelect:function(A){this.ne.nicCommand("hiliteColor",A);this.removePane()}});nicEditors.registerPlugin(nicPlugin,nicColorOptions); + + +var nicImageOptions = { + buttons : { + 'image' : {name : 'Add Image', type : 'nicImageButton', tags : ['IMG']} + } + +}; + +var nicImageButton=nicEditorAdvancedButton.extend({addPane:function(){this.im=this.ne.selectedInstance.selElm().parentTag("IMG");this.addForm({"":{type:"title",txt:"Add/Edit Image"},src:{type:"text",txt:"URL",value:"http://",style:{width:"150px"}},alt:{type:"text",txt:"Alt Text",style:{width:"100px"}},align:{type:"select",txt:"Align",options:{none:"Default",left:"Left",right:"Right"}}},this.im)},submit:function(B){var C=this.inputs.src.value;if(C==""||C=="http://"){alert("You must enter a Image URL to insert");return false}this.removePane();if(!this.im){var A="javascript:nicImTemp();";this.ne.nicCommand("insertImage",A);this.im=this.findElm("IMG","src",A)}if(this.im){this.im.setAttributes({src:this.inputs.src.value,alt:this.inputs.alt.value,align:this.inputs.align.value})}}});nicEditors.registerPlugin(nicPlugin,nicImageOptions); + + +var nicSaveOptions = { + buttons : { + 'save' : {name : __('Save this content'), type : 'nicEditorSaveButton'} + } +}; + +var nicEditorSaveButton=nicEditorButton.extend({init:function(){if(!this.ne.options.onSave){this.margin.setStyle({display:"none"})}},mouseClick:function(){var B=this.ne.options.onSave;var A=this.ne.selectedInstance;B(A.getContent(),A.elm.id,A)}});nicEditors.registerPlugin(nicPlugin,nicSaveOptions); + diff --git a/scp/js/scp.js b/scp/js/scp.js new file mode 100644 index 0000000000000000000000000000000000000000..4f61090529b84749a91139ef237308ed3a5e31ee --- /dev/null +++ b/scp/js/scp.js @@ -0,0 +1,188 @@ +/* + scp.js + + osTicket SCP + Copyright (c) osTicket.com + + */ + +function selectAll(formObj,task,highlight){ + var highlight = highlight || false; + + for (var i=0;i < formObj.length;i++){ + var e = formObj.elements[i]; + if (e.type == 'checkbox' && !e.disabled){ + if(task==0){ + e.checked =false; + }else if(task==1){ + e.checked = true; + }else{ + e.checked = (e.checked) ? false : true; + } + + if(highlight && 0) { + highLight(e.value,e.checked); + } + } + } + + return false; +} + +function reset_all(formObj){ + return selectAll(formObj,0,true); +} +function select_all(formObj,highlight){ + return selectAll(formObj,1,highlight); +} +function toogle_all(formObj,highlight){ + + var highlight = highlight || false; + return selectAll(formObj,2,highlight); +} + + + +function checkbox_checker(formObj, min,max) { + + + var checked=$("input[type=checkbox]:checked").length; + var action= action?action:"process"; + if (max>0 && checked > max ){ + msg="You're limited to only " + max + " selections.\n" + msg=msg + "You have made " + checked + " selections.\n" + msg=msg + "Please remove " + (checked-max) + " selection(s)." + alert(msg) + return (false); + } + + if (checked< min ){ + alert("Please make at least " + min + " selections. " + checked + " checked so far.") + return (false); + } + + return (true); +} + + +$(document).ready(function(){ + + $("input:not(.dp):visible:enabled:first").focus(); + $('table.list tbody tr:odd').addClass('odd'); + + if($.browser.msie) { + $('.inactive').mouseenter(function() { + var elem = $(this); + var ie_shadow = $('<div>').addClass('ieshadow').css({ + height:$('ul', elem).height() + }); + elem.append(ie_shadow); + }).mouseleave(function() { + $('.ieshadow').remove(); + }); + } + + $("form#save :input").change(function() { + var fObj = $(this).closest('form'); + if(!fObj.data('changed')){ + fObj.data('changed', true); + $('input[type=submit]', fObj).css('color', 'red'); + } + }); + + $("form#save :input[type=reset]").click(function() { + var fObj = $(this).closest('form'); + if(fObj.data('changed')){ + $('input[type=submit]', fObj).removeAttr('style'); + $('label', fObj).removeAttr('style'); + $('label', fObj).removeClass('strike'); + fObj.data('changed', false); + } + }); + + + $(".clearrule").live('click',function() { + $(this).closest("tr").find(":input").val(''); + return false; + }); + + + //Canned attachments. + $('#canned_attachments, #faq_attachments').delegate('input:checkbox', 'click', function(e) { + var elem = $(this); + if(!$(this).is(':checked') && confirm("Are you sure you want to remove this attachment?")==true) { + elem.parent().addClass('strike'); + } else { + elem.attr('checked', 'checked'); + elem.parent().removeClass('strike'); + } + }); + + $('form select#cannedResp').change(function() { + + var fObj=$(this).closest('form'); + var cannedId = $(this).val(); + var ticketId = $(':input[name=id]',fObj).val(); + + $(this).find('option:first').attr('selected', 'selected').parent('select'); + + $.ajax({ + type: "GET", + url: 'ajax.php/kb/canned-response/'+cannedId+'.json', + data: 'tid='+ticketId, + dataType: 'json', + cache: false, + success: function(canned){ + //Canned response. + if(canned.response) { + if($('#append',fObj).is(':checked') && $('#response',fObj).val()) + $('#response',fObj).val($('#response',fObj).val()+"\n\n"+canned.response+"\n"); + else + $('#response',fObj).val(canned.response); + } + //Canned attachments. + if(canned.files && $('#canned_attachments',fObj).length) { + $.each(canned.files,function(i, j) { + if(!$('#canned_attachments #f'+j.id,fObj).length) { + var file='<label><input type="checkbox" name="cannedattachments[]" value="' + j.id+'" id="f'+j.id+'" checked="checked">'; + file+= '<a href="file.php?h=' + j.hash + j.key+ '">'+ j.name +'</a></label>'; + $('#canned_attachments', fObj).append(file); + } + + }); + } + } + }) + .done(function() { }) + .fail(function() { }); + }); + + + + + /* global inits */ + + /* Get config settings from the backend */ + $.get('ajax.php/config/ui.json', + function(config){ + /* + if(config && config.max_attachments) + alert(config.max_attachments); + */ + }, + 'json') + .error( function() {}); + + /* NicEdit richtext init */ + var rtes = $('.richtext'); + var rtes_count = rtes.length; + for(i=0;i<rtes_count;i++) { + var initial_value = rtes[i].value; + rtes[i].id = 'rte-'+i; + new nicEditor({iconsPath:'images/nicEditorIcons.gif'}).panelInstance('rte-'+i); + if(initial_value=='') { + nicEditors.findEditor('rte-'+i).setContent(''); + } + } + +}); diff --git a/scp/js/ticket.js b/scp/js/ticket.js new file mode 100644 index 0000000000000000000000000000000000000000..902298ce43f64240a819282967a45f984ce2a424 --- /dev/null +++ b/scp/js/ticket.js @@ -0,0 +1,322 @@ +/********************************************************************* + ticket.js + + Ticket utility loaded on ticket view! + + Useful for UI config settings, ticket locking ...etc + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +var autoLock = { + + addEvent: function(elm, evType, fn, useCapture) { + if(elm.addEventListener) { + elm.addEventListener(evType, fn, useCapture); + return true; + }else if(elm.attachEvent) { + return elm.attachEvent('on' + evType, fn); + }else{ + elm['on' + evType] = fn; + } + }, + + removeEvent: function(elm, evType, fn, useCapture) { + if(elm.removeEventListener) { + elm.removeEventListener(evType, fn, useCapture); + return true; + }else if(elm.detachEvent) { + return elm.detachEvent('on' + evType, fn); + }else { + elm['on' + evType] = null; + } + }, + + //Incoming event... + handleEvent: function(e) { + if(!autoLock.lockId) { + var now = new Date().getTime(); + //Retry every 5 seconds?? + if(autoLock.retry && (!autoLock.lastattemptTime || (now-autoLock.lastattemptTime)>5000)) { + autoLock.acquireLock(e,autoLock.warn); + autoLock.lastattemptTime=new Date().getTime(); + } + }else{ + autoLock.renewLock(e); + } + + if(!autoLock.lasteventTime) //I hate nav away warnings..but + autoLock.addEvent(window,'beforeunload',autoLock.discardWarning,true); + + autoLock.lasteventTime=new Date().getTime(); + }, + + //Watch activity on individual form. + watchForm: function(fObj,fn) { + if(!fObj || !fObj.length) + return; + + //Watch onSubmit event on the form. + autoLock.addEvent(fObj,'submit',autoLock.onSubmit,true); + //Watch activity on text + textareas + select fields. + for (var i=0; i<fObj.length; i++) { + switch(fObj[i].type) { + case 'textarea': + case 'text': + autoLock.addEvent(fObj[i],'keyup',autoLock.handleEvent,true); + break; + case 'select-one': + case 'select-multiple': + if(fObj.name!='reply') //Bug on double ajax call since select make it's own ajax call. TODO: fix it + autoLock.addEvent(fObj[i],'change',autoLock.handleEvent,true); + break; + default: + } + } + }, + + //Watch all the forms on the document. + watchDocument: function() { + + //Watch forms of interest only. + for (var i=0; i<document.forms.length; i++) { + if(!document.forms[i].id.value || parseInt(document.forms[i].id.value)!=autoLock.tid) + continue; + autoLock.watchForm(document.forms[i],autoLock.checkLock); + } + }, + + Init: function(config) { + + //make sure we are on ticket view page! + void(autoLock.form=document.forms['reply']); + if(!autoLock.form || !autoLock.form.id.value) { + return; + } + + void(autoLock.tid=parseInt(autoLock.form.id.value)); + autoLock.lockId=0; + autoLock.timerId=0; + autoLock.lasteventTime=0; + autoLock.lastattemptTime=0; + autoLock.acquireTime=0; + autoLock.renewTime=0; + autoLock.renewFreq=0; //renewal frequency in seconds...based on returned lock time. + autoLock.time=0; + if(config && config.ticket_lock_time) + autoLock.timeTime=config.ticket_lock_time + + autoLock.lockAttempts=0; //Consecutive lock attempt errors + autoLock.maxattempts=2; //Maximum failed lock attempts before giving up. + autoLock.warn=true; + autoLock.retry=true; + autoLock.watchDocument(); + autoLock.resetTimer(); + autoLock.addEvent(window,'unload',autoLock.releaseLock,true); //Release lock regardless of any activity. + }, + + + onSubmit: function(e) { + if(e.type=='submit') { //Submit. double check! + //remove nav away warning if any. + autoLock.removeEvent(window,'beforeunload',autoLock.discardWarning,true); + //Only warn if we had a failed lock attempt. + if(autoLock.warn && !autoLock.lockId && autoLock.lasteventTime) { + var answer=confirm('Unable to acquire a lock on the ticket. Someone else could be working on the same ticket. \ + Please confirm if you wish to continue anyways.'); + if(!answer) { + e.returnValue=false; + e.cancelBubble=true; + if(e.preventDefault) { + e.preventDefault(); + } + return false; + } + } + } + return true; + }, + + acquireLock: function(e,warn) { + + if(!autoLock.tid) { return false; } + + var warn = warn || false; + + if(autoLock.lockId) { + autoLock.renewLock(e); + } else { + $.ajax({ + type: "GET", + url: 'ajax.php/ticket/'+autoLock.tid+'/lock', + dataType: 'json', + cache: false, + success: function(lock){ + autoLock.setLock(lock,'acquire',warn); + } + }) + .done(function() { }) + .fail(function() { }); + } + + return autoLock.lockId; + }, + + //Renewal only happens on form activity.. + renewLock: function(e) { + + if(!autoLock.lockId) { return false; } + + var now= new Date().getTime(); + if(!autoLock.lastcheckTime || (now-autoLock.lastcheckTime)>=(autoLock.renewFreq*1000)){ + $.ajax({ + type: 'POST', + url: 'ajax.php/ticket/'+autoLock.tid+'/lock/'+autoLock.lockId+'/renew', + dataType: 'json', + cache: false, + success: function(lock){ + autoLock.setLock(lock,'renew',autoLock.warn); + } + }) + .done(function() { }) + .fail(function() { }); + } + }, + + releaseLock: function(e) { + if(!autoLock.tid) { return false; } + + $.ajax({ + type: 'POST', + url: 'ajax.php/ticket/'+autoLock.tid+'/lock/'+autoLock.lockId+'/release', + data: 'delete', + cache: false, + success: function(){ + + } + }) + .done(function() { }) + .fail(function() { }); + }, + + setLock: function(lock, action, warn) { + var warn = warn || false; + + if(!lock) return false; + + if(lock.id) { + autoLock.renewFreq=lock.time?(lock.time/2):30; + autoLock.lastcheckTime=new Date().getTime(); + } + autoLock.lockId=lock.id; //overwrite the lockid. + + switch(action){ + case 'renew': + if(!lock.id && lock.retry) { + autoLock.lockAttempts=1; //reset retries. + autoLock.acquireLock(e,false); //We lost the lock?? ..try to re acquire now. + } + break; + case 'acquire': + if(!lock.id) { + autoLock.lockAttempts++; + if(warn && (!lock.retry || autoLock.lockAttempts>=autoLock.maxattempts)) { + autoLock.retry=false; + alert('Unable to lock the ticket. Someone else could be working on the same ticket.'); + } + } + break; + } + }, + + discardWarning: function(e) { + e.returnValue="Any changes or info you've entered will be discarded!"; + }, + + //TODO: Monitor events and elapsed time and warn user when the lock is about to expire. + monitorEvents: function() { + // warn user when lock is about to expire??; + //autoLock.resetTimer(); + }, + + clearTimer: function() { + clearTimeout(autoLock.timerId); + }, + + resetTimer: function() { + clearTimeout(autoLock.timerId); + autoLock.timerId=setTimeout(function () { autoLock.monitorEvents() },30000); + } +} + +/* + UI & form events +*/ + +jQuery(function($) { + $('#response_options form').hide(); + $('#ticket_notes').hide(); + if(location.hash != "" && $('#response_options '+location.hash).length) { + $('#response_options '+location.hash+'_tab').addClass('active'); + $('#response_options '+location.hash).show(); + } else if(location.hash == "#notes") { + $('#response_options #note_tab').addClass('active'); + $('#response_options form').hide(); + $('#response_options #note').show(); + $('#ticket_thread').hide(); + $('#ticket_notes').show(); + $('#toggle_ticket_thread').removeClass('active'); + $('#toggle_notes').addClass('active'); + } else { + $('#response_options ul li:first a').addClass('active'); + $('#response_options '+$('#response_options ul li:first a').attr('href')).show(); + } + + $('#reply_tab').click(function() { + $(this).removeClass('tell'); + }); + + $('#note_tab').click(function() { + if($('#response').val() != '') { + $('#reply_tab').addClass('tell'); + } + }); + + $('#response_options ul li a').click(function(e) { + e.preventDefault(); + $('#response_options ul li a').removeClass('active'); + $(this).addClass('active'); + $('#response_options form').hide(); + //window.location.hash = this.hash; + $('#response_options '+$(this).attr('href')).show(); + $("#msg_error, #msg_notice, #msg_warning").fadeOut(); + }); + + $('#toggle_ticket_thread, #toggle_notes, .show_notes').click(function(e) { + e.preventDefault(); + $('#threads a').removeClass('active'); + + if($(this).attr('id') == 'toggle_ticket_thread') { + $('#ticket_notes').hide(); + $('#ticket_thread').show(); + $('#toggle_ticket_thread').addClass('active'); + $('#reply_tab').removeClass('tell').click(); + } else { + $('#ticket_thread').hide(); + $('#ticket_notes').show(); + $('#toggle_notes').addClass('active'); + $('#note_tab').click(); + if($('#response').val() != '') { + $('#reply_tab').addClass('tell'); + } + } + }); + + //Start watching the form for activity. + autoLock.Init(); +}); diff --git a/scp/js/tips.js b/scp/js/tips.js new file mode 100644 index 0000000000000000000000000000000000000000..ec0f1875d55f31e5b8e9afcf283d7700183ea6e8 --- /dev/null +++ b/scp/js/tips.js @@ -0,0 +1,108 @@ +jQuery(function($) { + var tip_timer = 0; + var tips = $('.tip'); + for(i=0;i<tips.length;i++) { + tips[i].rel = 'tip-' + i; + } + + var showtip = function (url, elem,xoffset) { + + var pos = elem.offset(); + var y_pos = pos.top - 12; + var x_pos = pos.left + (xoffset || ((elem.width()/2) + 20)); + + var tip_arrow = $('<img>').attr('src', './images/tip_arrow.png').addClass('tip_arrow'); + var tip_box = $('<div>').addClass('tip_box'); + var tip_shadow = $('<div>').addClass('tip_shadow'); + var tip_content = $('<div>').addClass('tip_content').load(url, function() { + tip_content.prepend('<a href="#" class="tip_close">x</a>'); + }); + + var the_tip = tip_box.append(tip_arrow).append(tip_content).prepend(tip_shadow); + the_tip.css({ + "top":y_pos + "px", + "left":x_pos + "px" + }).addClass(elem.data('id')); + $('.tip_box').remove(); + $('body').append(the_tip.hide().fadeIn()); + $('.' + elem.data('id') + ' .tip_shadow').css({ + "height":$('.' + elem.data('id')).height() + 5 + }); + }; + + //Generic tip. + $('.tip').live('click mouseover', function(e) { + e.preventDefault(); + var id = this.rel; + var elem = $(this); + + elem.data('id',id); + elem.data('timer',0); + if($('.' + id).length == 0) { + if(e.type=='mouseover') { + /* wait about 1 sec - before showing the tip - mouseout kills the timeout*/ + elem.data('timer',setTimeout(function() { showtip('ajax.php/content/'+elem.attr('href'),elem);},750)) + }else{ + showtip('ajax.php/content/'+elem.attr('href'),elem); + } + } + }).live('mouseout', function(e) { + clearTimeout($(this).data('timer')); + }); + + //faq preview tip + $('.previewfaq').live('mouseover', function(e) { + e.preventDefault(); + var elem = $(this); + + var vars = elem.attr('href').split('='); + var url = 'ajax.php/kb/faq/'+vars[1]; + var id='faq'+vars[1]; + var xoffset = 100; + + elem.data('id',id); + elem.data('timer',0); + if($('.' + id).length == 0) { + if(e.type=='mouseover') { + /* wait about 1 sec - before showing the tip - mouseout kills the timeout*/ + elem.data('timer',setTimeout(function() { showtip(url,elem,xoffset);},750)) + }else{ + showtip(url,elem,xoffset); + } + } + }).live('mouseout', function(e) { + clearTimeout($(this).data('timer')); + }); + + //Ticket preview + $('.ticketPreview').live('mouseover', function(e) { + e.preventDefault(); + var elem = $(this); + + var vars = elem.attr('href').split('='); + var url = 'ajax.php/ticket/'+vars[1]+'/preview'; + var id='t'+vars[1]; + var xoffset = 80; + + + elem.data('id',id); + elem.data('timer',0); + if($('.' + id).length == 0) { + if(e.type=='mouseover') { + /* wait about 1 sec - before showing the tip - mouseout kills the timeout*/ + elem.data('timer',setTimeout(function() { showtip(url,elem,xoffset);},750)) + }else{ + showtip(url,elem,xoffset); + } + } + }).live('mouseout', function(e) { + clearTimeout($(this).data('timer')); + }); + + + + $('body').delegate('.tip_close', 'click', function(e) { + e.preventDefault(); + $(this).parent().parent().remove(); + }); +}); diff --git a/scp/kb.php b/scp/kb.php new file mode 100644 index 0000000000000000000000000000000000000000..0a683a05f01d8cf294b336929038a9803171ab85 --- /dev/null +++ b/scp/kb.php @@ -0,0 +1,30 @@ +<?php +/********************************************************************* + kb.php + + Knowlegebase + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.faq.php'); +$category=null; +if($_REQUEST['cid'] && !($category=Category::lookup($_REQUEST['cid']))) + $errors['err']='Unknown or invalid FAQ category'; + +$inc='kb-categories.inc.php'; //KB landing page. +if($category && $_REQUEST['a']!='search') { + $inc='kb-category.inc.php'; +} +$nav->setTabActive('kbase'); +require_once(STAFFINC_DIR.'header.inc.php'); +require_once(STAFFINC_DIR.$inc); +require_once(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/login.php b/scp/login.php new file mode 100644 index 0000000000000000000000000000000000000000..2af73c318b45ddb15438d3ab8966ea76d11c6576 --- /dev/null +++ b/scp/login.php @@ -0,0 +1,38 @@ +<?php +/********************************************************************* + login.php + + Handles staff authentication/logins + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once('../main.inc.php'); +if(!defined('INCLUDE_DIR')) die('Fatal Error. Kwaheri!'); + +require_once(INCLUDE_DIR.'class.staff.php'); + +$msg=$_SESSION['_staff']['auth']['msg']; +$msg=$msg?$msg:'Authentication Required'; +if($_POST && (!empty($_POST['username']) && !empty($_POST['passwd']))){ + //$_SESSION['_staff']=array(); #Uncomment to disable login strikes. + $msg='Invalid login'; + if(($user=Staff::login($_POST['username'],$_POST['passwd'],$errors))){ + $dest=$_SESSION['_user']['auth']['dest']; + $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php'; + @header("Location: $dest"); + require_once('index.php'); //Just incase header is messed up. + exit; + }elseif(!$errors['err']){ + $errors['err']='Login error - try again'; + } +} +define("OSTSCPINC",TRUE); //Make includes happy! +include_once(INCLUDE_DIR.'staff/login.tpl.php'); +?> diff --git a/scp/logout.php b/scp/logout.php new file mode 100644 index 0000000000000000000000000000000000000000..e02e4be1609d387d43b3e59d1693a291d80677cb --- /dev/null +++ b/scp/logout.php @@ -0,0 +1,25 @@ +<?php +/********************************************************************* + logout.php + + Log out staff + Destroy the session and redirect to login.php + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('staff.inc.php'); +Sys::log(LOG_DEBUG,'Staff logout',sprintf("%s logged out [%s]",$thisstaff->getUserName(),$_SERVER['REMOTE_ADDR'])); //Debug. +$_SESSION['_staff']=array(); +session_unset(); +session_destroy(); +session_write_close(); +@header('Location: login.php'); +require('login.php'); +?> diff --git a/scp/profile.php b/scp/profile.php new file mode 100644 index 0000000000000000000000000000000000000000..f9dd42a5f19a597e6b40c0e68774dce58d2a4a02 --- /dev/null +++ b/scp/profile.php @@ -0,0 +1,48 @@ +<?php +/********************************************************************* + profile.php + + Staff's profile handle + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require_once('staff.inc.php'); +$msg=''; +$staff=Staff::lookup($thisstaff->getId()); +if($_POST && $_POST['id']!=$thisstaff->getId()) { //Check dummy ID used on the form. + $errors['err']='Internal Error. Action Denied'; +} +if(!$errors && $_POST) { //Handle post + + if(!$staff) + $errors['err']='Unknown or invalid staff'; + elseif($staff->updateProfile($_POST,$errors)){ + $msg='Profile updated successfully'; + $thisstaff->reload(); + $staff->reload(); + $_SESSION['TZ_OFFSET']=$thisstaff->getTZoffset(); + $_SESSION['daylight']=$thisstaff->observeDaylight(); + }elseif(!$errors['err']) + $errors['err']='Profile update error. Try correcting the errors below and try again!'; +} + +//Forced password Change. +if($thisstaff->forcePasswdChange() && !$errors['err']) + $errors['err']=sprintf('<b>Hi %s</b> - You must change your password to continue!',$thisstaff->getFirstName()); +elseif($thisstaff->onVacation() && !$warn) + $warn=sprintf('<b>Welcome back %s</b>! You are listed as \'on vacation\' Please let your manager know that you are back.',$thisstaff->getFirstName()); + +$inc='profile.inc.php'; +$nav->setTabActive('dashboard'); +require_once(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$inc); +require_once(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/settings.php b/scp/settings.php new file mode 100644 index 0000000000000000000000000000000000000000..162ea816c844ec9702a2723a2ba672f391def96b --- /dev/null +++ b/scp/settings.php @@ -0,0 +1,63 @@ +<?php +/********************************************************************* + settings.php + + Handles all admin settings. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +$errors=array(); +$SettingOptions=array('general'=>'General Settings', + 'dates'=>'Date and Time Options', + 'tickets'=>'Ticket Settings and Options', + 'emails'=>'Email Settings', + 'attachments'=>'Attachments Settings', + 'kb'=>'Knowledgebase Settings', + 'autoresponders'=>'Autoresponder Settings', + 'alerts'=>'Alerts and Notices Settings'); + +//Handle a POST. +if($_POST && !$errors){ + $errors=array(); + if($cfg && $cfg->updateSettings($_POST,$errors)){ + $msg=Format::htmlchars($SettingOptions[$_POST['t']]).' Updated Successfully'; + $cfg->reload(); + }elseif(!$errors['err']){ + $errors['err']='Unable to update system settings - correct any errors below and try again'; + } +} + +$target=($_REQUEST['t'] && $SettingOptions[$_REQUEST['t']])?$_REQUEST['t']:'general'; + +$nav->setTabActive('settings'); +require(STAFFINC_DIR.'header.inc.php'); +?> +<h2>System Preferences and Settings - <span>osTicket (v<?php echo $cfg->getVersion(); ?>)</span></h2> +<div style="padding-top:10px;padding-bottom:5px;"> + <form method="get" action="settings.php"> + Setting Option: + <select name="t" style="width:300px;"> + <option value="">— Select Setting Group —</option> + <?php + foreach($SettingOptions as $k=>$v) { + $sel=($target==$k)?'selected="selected"':''; + echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); + } + ?> + </select> + <input type="submit" value="Go"> + </form> +</div> +<?php +$config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfig()); +include_once(STAFFINC_DIR."settings-$target.inc.php"); +include_once(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/slas.php b/scp/slas.php new file mode 100644 index 0000000000000000000000000000000000000000..c67a4d8cd007f7811b3aead54d67280388c33ed5 --- /dev/null +++ b/scp/slas.php @@ -0,0 +1,101 @@ +<?php +/********************************************************************* + slas.php + + SLA - Service Level Agreements + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.sla.php'); + +$sla=null; +if($_REQUEST['id'] && !($sla=SLA::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid API key ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$sla){ + $errors['err']='Unknown or invalid SLA plan.'; + }elseif($sla->update($_POST,$errors)){ + $msg='SLA plan updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating SLA plan. Try again!'; + } + break; + case 'add': + if(($id=SLA::create($_POST,$errors))){ + $msg='SLA plan added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add SLA plan. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one plan.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.SLA_TABLE.' SET isactive=1 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected SLA plans enabled'; + else + $warn="$num of $count selected SLA plans enabled"; + }else{ + $errors['err']='Unable to enable selected SLA plans.'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.SLA_TABLE.' SET isactive=0 WHERE id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected SLA plans disabled'; + else + $warn="$num of $count selected SLA plans disabled"; + }else{ + $errors['err']='Unable to disable selected SLA plans'; + } + + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($p=SLA::lookup($v)) && $p->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected SLA plans deleted successfully'; + elseif($i>0) + $warn="$i of $count selected SLA plans deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected SLA plans'; + + }else { + $errors['err']='Unknown action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='slaplans.inc.php'; +if($sla || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='slaplan.inc.php'; + +$nav->setTabActive('settings'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/staff.inc.php b/scp/staff.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..3c0d328809cbc94b37e9a2fee074ce6dabb08a97 --- /dev/null +++ b/scp/staff.inc.php @@ -0,0 +1,109 @@ +<?php +/************************************************************************* + staff.inc.php + + File included on every staff page...handles logins (security) and file path issues. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +if(basename($_SERVER['SCRIPT_NAME'])==basename(__FILE__)) die('Kwaheri rafiki!'); //Say hi to our friend.. + +if(!file_exists('../main.inc.php')) die('Fatal error... get technical support'); + +define('ROOT_PATH','../'); //Path to the root dir. +require_once('../main.inc.php'); + +if(!defined('INCLUDE_DIR')) die('Fatal error... invalid setting.'); + +/*Some more include defines specific to staff only */ +define('STAFFINC_DIR',INCLUDE_DIR.'staff/'); +define('SCP_DIR',str_replace('//','/',dirname(__FILE__).'/')); + +/* Define tag that included files can check */ +define('OSTSCPINC',TRUE); +define('OSTSTAFFINC',TRUE); + +/* Tables used by staff only */ +define('KB_PREMADE_TABLE',TABLE_PREFIX.'kb_premade'); + + +/* include what is needed on staff control panel */ + +require_once(INCLUDE_DIR.'class.staff.php'); +require_once(INCLUDE_DIR.'class.group.php'); +require_once(INCLUDE_DIR.'class.nav.php'); + +/* First order of the day is see if the user is logged in and with a valid session. + * User must be valid staff beyond this point + * ONLY super admins can access the helpdesk on offline state. +*/ + + +if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the function to trap expired sessions. + function staffLoginPage($msg) { + $_SESSION['_staff']['auth']['dest']=THISPAGE; + $_SESSION['_staff']['auth']['msg']=$msg; + require(SCP_DIR.'login.php'); + exit; + } +} + +$thisstaff = new StaffSession($_SESSION['_staff']['userID']); //Set staff object. +//1) is the user Logged in for real && is staff. +if(!$thisstaff || !is_object($thisstaff) || !$thisstaff->getId() || !$thisstaff->isValid()){ + $msg=(!$thisstaff || !$thisstaff->isValid())?'Authentication Required':'Session timed out due to inactivity'; + staffLoginPage($msg); + exit; +} +//2) if not super admin..check system status and group status +if(!$thisstaff->isadmin()){ + //Staff are not allowed to login in offline mode!! + if($cfg->isHelpDeskOffline()){ + staffLoginPage('System Offline'); + exit; + } + //Check for disabled staff or group! + if(!$thisstaff->isactive() || !$thisstaff->isGroupActive()) { + staffLoginPage('Access Denied. Contact Admin'); + exit; + } +} + + +//Keep the session activity alive +$thisstaff->refreshSession(); +//Set staff's timezone offset. +$_SESSION['TZ_OFFSET']=$thisstaff->getTZoffset(); +$_SESSION['daylight']=$thisstaff->observeDaylight(); + +define('AUTO_REFRESH_RATE',$thisstaff->getRefreshRate()*60); + +//Clear some vars. we use in all pages. +$errors=array(); +$msg=$warn=$sysnotice=''; +$tabs=array(); +$submenu=array(); + +if(defined('THIS_VERSION') && strcasecmp($cfg->getVersion(),THIS_VERSION)) { + $errors['err']=$sysnotice=sprintf('The script is version %s while the database is version %s',THIS_VERSION,$cfg->getVersion()); +}elseif($cfg->isHelpDeskOffline()){ + $sysnotice='<strong>System is set to offline mode</strong> - Client interface is disabled and ONLY admins can access staff control panel.'; + $sysnotice.=' <a href="settings.php">Enable</a>.'; +} + +$nav = new StaffNav($thisstaff); +//Check for forced password change. +if($thisstaff->forcePasswdChange()){ + # XXX: Call staffLoginPage() for AJAX and API requests _not_ to honor + # the request + require('profile.php'); //profile.php must request this file as require_once to avoid problems. + exit; +} +?> diff --git a/scp/staff.php b/scp/staff.php new file mode 100644 index 0000000000000000000000000000000000000000..863a348c9f37f547f5a8b57ddf0f162cc5257b1f --- /dev/null +++ b/scp/staff.php @@ -0,0 +1,99 @@ +<?php +/********************************************************************* + staff.php + + Evertything about staff members. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +$staff=null; +if($_REQUEST['id'] && !($staff=Staff::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid staff ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$staff){ + $errors['err']='Unknown or invalid staff.'; + }elseif($staff->update($_POST,$errors)){ + $msg='Staff updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Unable to update staff. Correct any error(s) below and try again!'; + } + break; + case 'create': + if(($id=Staff::create($_POST,$errors))){ + $msg=Format::htmlchars($_POST['name']).' added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add staff. Correct any error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one staff member.'; + }elseif(in_array($thisstaff->getId(),$_POST['ids'])) { + $errors['err']='You can not disable/delete yourself - you could be the only admin!'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.STAFF_TABLE.' SET isactive=1 WHERE staff_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected staff activated'; + else + $warn="$num of $count selected staff activated"; + }else{ + $errors['err']='Unable to activate selected staff'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.STAFF_TABLE.' SET isactive=0 '. + 'WHERE staff_id IN ('.implode(',',$_POST['ids']).') AND staff_id!='.db_input($thisstaff->getId()); + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected staff disabled'; + else + $warn="$num of $count selected staff disabled"; + }else{ + $errors['err']='Unable to disable selected staff'; + } + }elseif($_POST['delete']){ + foreach($_POST['ids'] as $k=>$v) { + if($v!=$thisstaff->getId() && ($s=Staff::lookup($v)) && $s->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected staff deleted successfully'; + elseif($i>0) + $warn="$i of $count selected staff deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected staff.'; + }else{ + $errors['err']='Unknown action. Get technical help!'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='staffmembers.inc.php'; +if($staff || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='staff.inc.php'; + +$nav->setTabActive('staff'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/syslogs.php b/scp/syslogs.php new file mode 100644 index 0000000000000000000000000000000000000000..843fecd842cf49e3d3622338aec78777755ba544 --- /dev/null +++ b/scp/syslogs.php @@ -0,0 +1,52 @@ +<?php +/********************************************************************* + syslogs.php + + System Logs + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one log to delete'; + }else{ + $count=count($_POST['ids']); + if($_POST['delete']){ + $sql='DELETE FROM '.SYSLOG_TABLE.' WHERE log_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected logs deleted successfully'; + else + $warn="$num of $count selected logs deleted"; + }elseif(!$errors['err']) + $errors['err']='Unable to delete selected logs'; + }else{ + $errors['err']='Unknown command'; + } + } + break; + default: + $errors['err']='Unknown option'; + break; + } +} + + + +$page='syslogs.inc.php'; +$nav->setTabActive('dashboard'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/teams.php b/scp/teams.php new file mode 100644 index 0000000000000000000000000000000000000000..c579372197995e4fd3811eddf338d457817aadb0 --- /dev/null +++ b/scp/teams.php @@ -0,0 +1,96 @@ +<?php +/********************************************************************* + teams.php + + Evertything about teams + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +$team=null; +if($_REQUEST['id'] && !($team=Team::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid team ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'update': + if(!$team){ + $errors['err']='Unknown or invalid team.'; + }elseif($team->update($_POST,$errors)){ + $msg='Team updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Unable to update team. Correct any error(s) below and try again!'; + } + break; + case 'create': + if(($id=Team::create($_POST,$errors))){ + $msg=Format::htmlchars($_POST['team']).' added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add team. Correct any error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one team.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.TEAM_TABLE.' SET isenabled=1 WHERE team_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected teams activated'; + else + $warn="$num of $count selected teams activated"; + }else{ + $errors['err']='Unable to activate selected teams'; + } + }elseif($_POST['disable']){ + $sql='UPDATE '.TEAM_TABLE.' SET isenabled=0 WHERE team_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg='Selected teams disabled'; + else + $warn="$num of $count selected teams disabled"; + }else{ + $errors['err']='Unable to disable selected teams'; + } + }elseif($_POST['delete']){ + foreach($_POST['ids'] as $k=>$v) { + if(($t=Team::lookup($v)) && $t->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected teams deleted successfully'; + elseif($i>0) + $warn="$i of $count selected teams deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected teams'; + }else{ + $errors['err']='Unknown action. Get technical help!'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='teams.inc.php'; +if($team || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='team.inc.php'; + +$nav->setTabActive('staff'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/templates.php b/scp/templates.php new file mode 100644 index 0000000000000000000000000000000000000000..47e24dd6e4a4d06d6e35cf8d0ccae290b99dbd24 --- /dev/null +++ b/scp/templates.php @@ -0,0 +1,116 @@ +<?php +/********************************************************************* + templates.php + + Email Templates + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('admin.inc.php'); +include_once(INCLUDE_DIR.'class.template.php'); +$template=null; +if($_REQUEST['id'] && !($template=Template::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid template ID.'; + +if($_POST){ + switch(strtolower($_POST['do'])){ + case 'updatetpl': + if(!$template){ + $errors['err']='Unknown or invalid template'; + }elseif($template->updateMsgTemplate($_POST,$errors)){ + $template->reload(); + $msg='Message template updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating message template. Try again!'; + } + break; + case 'update': + if(!$template){ + $errors['err']='Unknown or invalid template'; + }elseif($template->update($_POST,$errors)){ + $msg='Template updated successfully'; + }elseif(!$errors['err']){ + $errors['err']='Error updating template. Try again!'; + } + break; + case 'add': + if((Template::create($_POST,$errors))){ + $msg='Template added successfully'; + $_REQUEST['a']=null; + }elseif(!$errors['err']){ + $errors['err']='Unable to add template. Correct error(s) below and try again.'; + } + break; + case 'mass_process': + if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err']='You must select at least one template to process.'; + }else{ + $count=count($_POST['ids']); + if($_POST['enable']){ + $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET isactive=1 WHERE tpl_id IN ('.implode(',',$_POST['ids']).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg='Selected templates enabled'; + else + $warn="$num of $count selected templates enabled"; + }else{ + $errors['err']='Unable to enable selected templates'; + } + }elseif($_POST['disable']){ + + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=Template::lookup($v)) && !$t->isInUse() && $t->disable()) + $i++; + } + + if($i && $i==$count) + $msg='Selected templates disabled'; + elseif($i) + $warn="$i of $count selected templates disabled (in-use templates can't be disabled)"; + else + $errors['err']="Unable to disable selected templates (in-use or default template can't be disabled)"; + }elseif($_POST['delete']){ + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=Template::lookup($v)) && $t->delete()) + $i++; + } + + if($i && $i==$count) + $msg='Selected templates deleted successfully'; + elseif($i>0) + $warn="$i of $count selected templates deleted"; + elseif(!$errors['err']) + $errors['err']='Unable to delete selected templates'; + + }else { + $errors['err']='Unknown template action'; + } + } + break; + default: + $errors['err']='Unknown action'; + break; + } +} + +$page='templates.inc.php'; +if($template && !strcasecmp($_REQUEST['a'],'manage')){ + $page='tpl.inc.php'; +}elseif($template || !strcasecmp($_REQUEST['a'],'add')){ + $page='template.inc.php'; +} + +$nav->setTabActive('emails'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/tickets.php b/scp/tickets.php new file mode 100644 index 0000000000000000000000000000000000000000..da9dfa97e1ea8f37df2fcc718a950da6027d12c7 --- /dev/null +++ b/scp/tickets.php @@ -0,0 +1,470 @@ +<?php +/************************************************************************* + tickets.php + + Handles all tickets related actions. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.dept.php'); +require_once(INCLUDE_DIR.'class.filter.php'); +require_once(INCLUDE_DIR.'class.canned.php'); + + +$page=''; +$ticket=null; //clean start. +//LOCKDOWN...See if the id provided is actually valid and if the user has access. +if($_REQUEST['id']) { + if(!($ticket=Ticket::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid ticket ID'; + elseif(!$ticket->checkStaffAccess($thisstaff)) { + $errors['err']='Access denied. Contact admin if you believe this is in error'; + $ticket=null; //Clear ticket obj. + } +} +//At this stage we know the access status. we can process the post. +if($_POST && !$errors): + + if($ticket && $ticket->getId()) { + //More coffee please. + $errors=array(); + $lock=$ticket->getLock(); //Ticket lock if any + $statusKeys=array('open'=>'Open','Reopen'=>'Open','Close'=>'Closed'); + switch(strtolower($_POST['a'])): + case 'reply': + + if(!$_POST['msgId']) + $errors['err']='Missing message ID - Internal error'; + if(!$_POST['response']) + $errors['response']='Response required'; + //Use locks to avoid double replies + if($lock && $lock->getStaffId()!=$thisstaff->getId()) + $errors['err']='Action Denied. Ticket is locked by someone else!'; + + //Make sure the email is not banned + if(!$errors['err'] && EmailFilter::isBanned($ticket->getEmail())) + $errors['err']='Email is in banlist. Must be removed to reply.'; + + $wasOpen =($ticket->isopen()); + //If no error...do the do. + if(!$errors && ($respId=$ticket->postReply($_POST,$_FILES['attachments'],$errors))) { + $msg='Reply posted successfully'; + $ticket->reload(); + if($ticket->isClosed() && $wasOpen) + $ticket=null; + } elseif(!$errors['err']) { + $errors['err']='Unable to post the reply. Correct the errors below and try again!'; + } + break; + case 'transfer': /** Transfer ticket **/ + //Check permission + if($thisstaff && $thisstaff->canTransferTickets()) { + if(!$_POST['deptId']) + $errors['deptId']='Select department'; + elseif($_POST['deptId']==$ticket->getDeptId()) + $errors['deptId']='Ticket already in the Dept.'; + elseif(!($dept=Dept::lookup($_POST['deptId']))) + $errors['deptId']='Unknown or invalid department'; + + if(!$_POST['transfer_message']) + $errors['transfer_message'] = 'Transfer comments/notes required'; + elseif(strlen($_POST['transfer_message'])<5) + $errors['transfer_message'] = 'Transfer comments too short!'; + + $currentDept = $ticket->getDeptName(); //save current dept name. + if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_message'])) { + $msg = 'Ticket transferred successfully to '.$ticket->getDeptName(); + //ticket->transfer does a reload...new dept at this point. + $title='Dept. Transfer from '.$currentDept.' to '.$ticket->getDeptName(); + /*** log the message as internal note - with alerts disabled - ***/ + $ticket->postNote($title, $_POST['transfer_message'], false); + //Check to make sure the staff still has access to the ticket + if(!$ticket->checkStaffAccess($thisstaff)) + $ticket=null; + + } else { + $errors['transfer']='Unable to complete the transfer - try again'; + $errors['err']='Missing or invalid data. Correct the error(s) below and try again!'; + } + } else { + $errors['err']=$errors['transfer']='Action Denied. You are not allowed to transfer tickets.'; + } + break; + case 'assign': + + if($thisstaff && $thisstaff->canAssignTickets()) { + if(!$_POST['assignId']) + $errors['assignId'] = 'Select assignee'; + elseif($_POST['assignId'][0]!='s' && $_POST['assignId'][0]!='t') + $errors['assignId']='Invalid assignee ID - get technical support'; + elseif($ticket->isAssigned()) { + $id=preg_replace("/[^0-9]/", "",$_POST['assignId']); + if($_POST['assignId'][0]=='s' && $id==$ticket->getStaffId()) + $errors['assignId']='Ticket already assigned to the staff.'; + elseif($_POST['assignId'][0]=='t' && $id==$ticket->getTeamId()) + $errors['assignId']='Ticket already assigned to the team.'; + } + + if(!$_POST['assign_message']) + $errors['assign_message']='Comments required'; + elseif(strlen($_POST['assign_message'])<5) + $errors['assign_message']='Comments too short'; + + if(!$errors && $ticket->assign($_POST['assignId'],$_POST['assign_message'])) { + $msg='Ticket assigned successfully to '.$ticket->getAssignee(); + TicketLock::removeStaffLocks($thisstaff->getId(),$ticket->getId()); + $ticket=null; + }elseif(!$errors['err']) { + $errors['err']='Unable to assign the ticket. Correct the errors below and try again.'; + } + + } else { + $errors['err']=$errors['assign']='Action Denied. You are not allowed to assign/reassign tickets.'; + } + break; + case 'postnote': /* Post Internal Note */ + $fields=array(); + $fields['title'] = array('type'=>'string', 'required'=>1, 'error'=>'Title required'); + $fields['internal_note'] = array('type'=>'string', 'required'=>1, 'error'=>'Note message required'); + + if(!Validator::process($fields, $_POST, $errors) && !$errors['err']) + $errors['err']=$errors['note']='Missing or invalid data. Correct the error(s) below and try again!'; + + if(!$errors && ($noteId=$ticket->postNote($_POST['title'], $_POST['internal_note']))) { + $msg='Internal note posted successfully'; + //Upload attachments IF ANY - TODO: validate attachment types?? + if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + $ticket->uploadAttachments($files,$noteId,'N'); + //Set state: Error on state change not critical! + if(isset($_POST['note_ticket_state']) && $_POST['note_ticket_state']) { + if($ticket->setState($_POST['note_ticket_state']) && $ticket->reload()) { + $msg.=' and state changed to '.strtoupper($_POST['note_ticket_state']); + if($ticket->isClosed()) + $ticket=null; //Going back to main listing. + } + } + } elseif(!$errors['note']) { + $errors['note']='Error(s) occurred. Unable to post the note.'; + } + break; + case 'update': + $page='editticket.inc.php'; + if(!$ticket || !$thisstaff->canEditTickets()) + $errors['err']='Perm. Denied. You are not allowed to edit tickets'; + elseif($ticket->update($_POST,$errors)){ + $msg='Ticket updated successfully'; + $page='ticket.inc.php'; + }elseif(!$errors['err']) { + $errors['err']='Error(s) occured! Try again.'; + } + break; + case 'process': + $isdeptmanager=($ticket->getDeptId()==$thisstaff->getDeptId())?true:false; + switch(strtolower($_POST['do'])): + case 'change_priority': + if(!$thisstaff->canManageTickets() && !$thisstaff->isManager()){ + $errors['err']='Perm. Denied. You are not allowed to change ticket\'s priority'; + }elseif(!$_POST['ticket_priority'] or !is_numeric($_POST['ticket_priority'])){ + $errors['err']='You must select priority'; + } + if(!$errors){ + if($ticket->setPriority($_POST['ticket_priority'])){ + $msg='Priority Changed Successfully'; + $ticket->reload(); + $note='Ticket priority set to "'.$ticket->getPriority().'" by '.$thisstaff->getName(); + $ticket->logActivity('Priority Changed',$note); + }else{ + $errors['err']='Problems changing priority. Try again'; + } + } + break; + case 'close': + if(!$thisstaff->isadmin() && !$thisstaff->canCloseTickets()){ + $errors['err']='Perm. Denied. You are not allowed to close tickets.'; + }else{ + if($ticket->close()){ + $msg='Ticket #'.$ticket->getExtId().' status set to CLOSED'; + $note='Ticket closed without response by '.$thisstaff->getName(); + $ticket->logActivity('Ticket Closed',$note); + $page=$ticket=null; //Going back to main listing. + }else{ + $errors['err']='Problems closing the ticket. Try again'; + } + } + break; + case 'reopen': + //if they can close...then assume they can reopen. + if(!$thisstaff->isadmin() && !$thisstaff->canCloseTickets()){ + $errors['err']='Perm. Denied. You are not allowed to reopen tickets.'; + }else{ + if($ticket->reopen()){ + $msg='Ticket status set to OPEN'; + $note='Ticket reopened (without comments)'; + if($_POST['ticket_priority']) { + $ticket->setPriority($_POST['ticket_priority']); + $ticket->reload(); + $note.=' and status set to '.$ticket->getPriority(); + } + $note.=' by '.$thisstaff->getName(); + $ticket->logActivity('Ticket Reopened',$note); + }else{ + $errors['err']='Problems reopening the ticket. Try again'; + } + } + break; + case 'release': + if(!($staff=$ticket->getStaff())) + $errors['err']='Ticket is not assigned!'; + elseif($ticket->release()) { + $msg='Ticket released (unassigned) from '.$staff->getName().' by '.$thisstaff->getName();; + $ticket->logActivity('Ticket unassigned',$msg); + }else + $errors['err']='Problems releasing the ticket. Try again'; + break; + case 'overdue': + //Mark the ticket as overdue + if(!$thisstaff->isadmin() && !$thisstaff->isManager()){ + $errors['err']='Perm. Denied. You are not allowed to flag tickets overdue'; + }else{ + if($ticket->markOverdue()){ + $msg='Ticket flagged as overdue'; + $note=$msg; + if($_POST['ticket_priority']) { + $ticket->setPriority($_POST['ticket_priority']); + $ticket->reload(); + $note.=' and status set to '.$ticket->getPriority(); + } + $note.=' by '.$thisstaff->getName(); + $ticket->logActivity('Ticket Marked Overdue',$note); + }else{ + $errors['err']='Problems marking the the ticket overdue. Try again'; + } + } + break; + case 'banemail': + if(!$thisstaff->isadmin() && !$thisstaff->canManageBanList()){ + $errors['err']='Perm. Denied. You are not allowed to ban emails'; + }elseif(Banlist::add($ticket->getEmail(),$thisstaff->getName())){ + $msg='Email ('.$ticket->getEmail().') added to banlist'; + if($ticket->isOpen() && $ticket->close()) { + $msg.=' & ticket status set to closed'; + $ticket->logActivity('Ticket Closed',$msg); + $page=$ticket=null; //Going back to main listing. + } + }else{ + $errors['err']='Unable to add the email to banlist'; + } + break; + case 'unbanemail': + if(!$thisstaff->isadmin() && !$thisstaff->canManageBanList()){ + $errors['err']='Perm. Denied. You are not allowed to remove emails from banlist.'; + }elseif(Banlist::remove($ticket->getEmail())){ + $msg='Email removed from banlist'; + }else{ + $errors['err']='Unable to remove the email from banlist. Try again.'; + } + break; + case 'delete': // Dude what are you trying to hide? bad customer support?? + if(!$thisstaff->isadmin() && !$thisstaff->canDeleteTickets()){ + $errors['err']='Perm. Denied. You are not allowed to DELETE tickets!!'; + }else{ + if($ticket->delete()){ + $page='tickets.inc.php'; //ticket is gone...go back to the listing. + $msg='Ticket Deleted Forever'; + $ticket=null; //clear the object. + }else{ + $errors['err']='Problems deleting the ticket. Try again'; + } + } + break; + default: + $errors['err']='You must select action to perform'; + endswitch; + break; + default: + $errors['err']='Unknown action'; + endswitch; + if($ticket && is_object($ticket)) + $ticket->reload();//Reload ticket info following post processing + }elseif($_POST['a']) { + switch($_POST['a']) { + case 'mass_process': + if(!$thisstaff->canManageTickets()) + $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access'; + elseif(!$_POST['tids'] || !is_array($_POST['tids'])) + $errors['err']='No tickets selected. You must select at least one ticket.'; + elseif(($_POST['reopen'] || $_POST['close']) && !$thisstaff->canCloseTickets()) + $errors['err']='You do not have permission to close/reopen tickets'; + elseif($_POST['delete'] && !$thisstaff->canDeleteTickets()) + $errors['err']='You do not have permission to delete tickets'; + elseif(!$_POST['tids'] || !is_array($_POST['tids'])) + $errors['err']='You must select at least one ticket'; + + if(!$errors) { + $count=count($_POST['tids']); + if(isset($_POST['reopen'])){ + $i=0; + $note='Ticket reopened by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + $t = new Ticket($v); + if($t && @$t->reopen()) { + $i++; + $t->logActivity('Ticket Reopened',$note,false,'System'); + } + } + $msg="$i of $count selected tickets reopened"; + }elseif(isset($_POST['close'])){ + $i=0; + $note='Ticket closed without response by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + $t = new Ticket($v); + if($t && @$t->close()){ + $i++; + $t->logActivity('Ticket Closed',$note,false,'System'); + } + } + $msg="$i of $count selected tickets closed"; + }elseif(isset($_POST['overdue'])){ + $i=0; + $note='Ticket flagged as overdue by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + $t = new Ticket($v); + if($t && !$t->isoverdue()) + if($t->markOverdue()) { + $i++; + $t->logActivity('Ticket Marked Overdue',$note,false,'System'); + } + } + $msg="$i of $count selected tickets marked overdue"; + }elseif(isset($_POST['delete'])){ + $i=0; + foreach($_POST['tids'] as $k=>$v) { + $t = new Ticket($v); + if($t && @$t->delete()) $i++; + } + $msg="$i of $count selected tickets deleted"; + } + } + break; + case 'open': + $ticket=null; + if(!$thisstaff || !$thisstaff->canCreateTickets()) { + $errors['err']='You do not have permission to create tickets. Contact admin for such access'; + }elseif(($ticket=Ticket::open($_POST, $_FILES['attachments'], $errors))) { + $msg='Ticket created successfully'; + $_REQUEST['a']=null; + if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) + $ticket=null; + }elseif(!$errors['err']) { + $errors['err']='Unable to create the ticket. Correct the error(s) and try again'; + } + break; + } + } + if(!$errors) + $thisstaff ->resetStats(); //We'll need to reflect any changes just made! +endif; + +/*... Quick stats ...*/ +$stats= $thisstaff->getTicketsStats(); + +// Switch queues on the fly! depending on stats +if(!$stats['open'] && $_REQUEST['a']!='search' && (!$_REQUEST['status'] || $_REQUEST['status']=='open')) { + if(!$cfg->showAnsweredTickets() && $stats['answered']) + $_REQUEST['status']= 'answered'; + else + $_REQUEST['status']= 'closed'; +} + +//Navigation +$nav->setTabActive('tickets'); +if($cfg->showAnsweredTickets()) { + $nav->addSubMenu(array('desc'=>'Open ('.($stats['open']+$stats['answered']).')', + 'title'=>'Open Tickets', + 'href'=>'tickets.php', + 'iconclass'=>'Ticket'), + (!$_REQUEST['status'] || $_REQUEST['status']=='open')); +} else { + + if(!$stats || $stats['open']) { + $nav->addSubMenu(array('desc'=>'Open ('.$stats['open'].')', + 'title'=>'Open Tickets', + 'href'=>'tickets.php', + 'iconclass'=>'Ticket'), + (!$_REQUEST['status'] || $_REQUEST['status']=='open')); + } + + if($stats['answered']) { + $nav->addSubMenu(array('desc'=>'Answered ('.$stats['answered'].')', + 'title'=>'Answered Tickets', + 'href'=>'tickets.php?status=answered', + 'iconclass'=>'answeredTickets'), + ($_REQUEST['status']=='answered')); + } +} + +if($stats['assigned']) { + if(!$sysnotice && $stats['assigned']>10) + $sysnotice=$stats['assigned'].' assigned to you!'; + + $nav->addSubMenu(array('desc'=>'My Tickets ('.$stats['assigned'].')', + 'title'=>'Assigned Tickets', + 'href'=>'tickets.php?status=assigned', + 'iconclass'=>'assignedTickets'), + ($_REQUEST['status']=='assigned')); +} + +if($stats['overdue']) { + $nav->addSubMenu(array('desc'=>'Overdue ('.$stats['overdue'].')', + 'title'=>'Stale Tickets', + 'href'=>'tickets.php?status=overdue', + 'iconclass'=>'overdueTickets'), + ($_REQUEST['status']=='overdue')); + + if(!$sysnotice && $stats['overdue']>10) + $sysnotice=$stats['overdue'] .' overdue tickets!'; +} + +$nav->addSubMenu(array('desc'=>'Closed Tickets', + 'title'=>'Closed Tickets', + 'href'=>'tickets.php?status=closed', + 'iconclass'=>'closedTickets'), + ($_REQUEST['status']=='closed')); + + +if($thisstaff->canCreateTickets()) { + $nav->addSubMenu(array('desc'=>'New Ticket', + 'href'=>'tickets.php?a=open', + 'iconclass'=>'newTicket'), + ($_REQUEST['a']=='open')); +} + + +$inc = 'tickets.inc.php'; +if($ticket) { + $nav->setActiveSubMenu(-1); + $inc = 'ticket-view.inc.php'; + if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) + $errors['err'] ='Work in progress... '; +}else { + $inc = 'tickets.inc.php'; + if($_REQUEST['a']=='open' && $thisstaff->canCreateTickets()) + $inc = 'ticket-open.inc.php'; + elseif(!$_POST && $_REQUEST['a']!='search' && ($min=$thisstaff->getRefreshRate())) + define('AUTO_REFRESH',1); //set refresh rate if the user has it configured +} + +require_once(STAFFINC_DIR.'header.inc.php'); +require_once(STAFFINC_DIR.$inc); +require_once(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/setup/cleanup.php b/setup/cleanup.php new file mode 100644 index 0000000000000000000000000000000000000000..1309228eb6c64eaaee6f16926ec98b06d91d0221 --- /dev/null +++ b/setup/cleanup.php @@ -0,0 +1,24 @@ +<?php +/********************************************************************* + cleanup.php + + Cleanup script called via ajax to migrate attachments. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +session_start(); +if($_GET['c']>10) { //When Done send 304 - nothing else to do. + $_SESSION['s']='done'; + session_write_close(); + header("HTTP/1.1 304 Not Modified"); + exit; +} +echo "Cleaning up...".time(); +?> diff --git a/setup/css/wizard.css b/setup/css/wizard.css new file mode 100644 index 0000000000000000000000000000000000000000..6fd18e17854f65a30a2947460a955ca4c403541c --- /dev/null +++ b/setup/css/wizard.css @@ -0,0 +1,78 @@ +body { background: url('../images/background.jpg?1312906017') top left repeat-x white; font-family: arial, helvetica, sans-serif; font-size: 10pt; color: #000; margin: 0; padding: 0; } + +#wizard { background: #fff; width: 800px; margin: 30px auto; padding: 10px; border: 1px solid #2a67ac; border-right: 2px solid #2a67ac; border-bottom: 3px solid #2a67ac; overflow: hidden; margin-bottom:5px;} + +a { color: #2a67ac; } + +/* Helpers */ +.centered {text-align: center;} +.clear { clear:both; height: 1px; visibility: none;} +.hidden { display: none;} +.error { color:#f00;} + +#header { height: 72px; margin-bottom: 20px; } +#header #logo { width: 280px; height: 72px; display: block; float: left; } +#header ul { margin: 0; padding: 0; width: 400px; float: right; text-align: right; } +#header ul .info { font-size: 11pt; font-weight: bold; border-bottom: 1px solid #2a67ac; color: #444; } +#header ul li { list-style: none; margin: 5px 0 0 0; padding: 0; } +#header ul li a { color: #F3811C; text-decoration: none; } +#header ul li a:hover { color: #2a67ac; } + +#sidebar { width: 180px; padding: 10px; border: 1px solid #C8DDFA; float: right; background: #F7FBFE; } +#sidebar h3 { font-size: 10pt; margin: 0 0 5px 0; padding: 0; text-indent: 32px; background: url('../images/cog.png?1312913866') top left no-repeat; line-height: 24px; color: #2a67ac; } + +#footer { color:#ccc; } +#footer a, #footer a:hover { text-decoration:none; color:#ccc; } + +#main { width: 580px; float: left; } +#main h1 { margin: 0; padding: 0; fontsize: 21pt; font-weight: normal; } +#main h2 { font-size: 15pt; margin: 0; padding: 0; } +#main h3 { font-size: 10pt; margin: 0; padding: 0; } +#main div#intro { padding-bottom: 5px; margin-bottom:10px; border-bottom: 1px solid #aaaaaa; } +#main { padding-bottom: 20px; } + +#main.step2 { width: 800px !important; } + +ul.progress { margin: 10px 0 20px 20px; padding: 0; } +ul.progress li { list-style: none; margin: 0; padding: 0 0 2px 24px; background: url('../images/incomplete.png?1312910844'); background-repeat: no-repeat; background-position: 0 50%; } +ul.progress li.yes { background-image: url('../images/yes.png?1312906296'); } +ul.progress li.no { background-image: url('../images/no.png?1312906277'); } + +ul.progress li.yes small {color:green; } +ul.progress li.no small {color:red;} + +#bar { clear: both; padding-top: 10px; height: 24px; line-height: 24px; text-align: center; border-top: 1px solid #aaaaaa; } +#bar a, #bar .btn { display: inline-block; margin: 0; height: 24px; line-height: 24px; font-weight: bold; border: 1px solid #666666; text-decoration: none; padding: 0 10px; background: url('../images/grey_btn_bg.png?1312910883') top left repeat-x; color: #333; } +#bar a:hover, #bar .btn:hover, #bar .btnover { background-position: bottom left; } +#bar a.unstyled, #bar a.unstyled:hover { font-weight: normal; background: none; border: none; text-decoration: underline; color: #2a67ac; } + +#bar.error { background: #ffd; text-align: center; color: #a00; font-weight: bold; } + +form { margin: 0; padding: 0; } +form h4.head { display: block; clear: both; border-top: 1px solid #2a67ac; height: 16px; line-height: 16px; padding: 5px 5px 5px 24px; margin: 10px 0 0 0; font-size: 10pt; background-position: 5px 50%; background-repeat: no-repeat; background-color: #e7f1fd; } +form span.subhead { display: block; background: #f4f4f4; border-bottom: 1px solid #cccccc; margin-bottom: 3px; padding: 3px 5px 3px 24px; font-style: italic; font-size: 9pt; } +form h4.system { background-image: url('../images/system.png?1262713696'); } +form h4.admin { background-image: url('../images/user.png?1262713720'); } +form h4.database { background-image: url('../images/database.png?1262713698'); } +form h4.email { background-image: url('../images/email.png?1142218352'); } +form .row { clear: both; padding: 2px 0 0 24px; background: #F7FBFE; height: 24px; } +form .row input { background: #fff; height: 22px; padding: 0; line-height: 22px; border: 1px solid #cccccc; } +form .row label, form .row span { display: block; float: left; width: 150px; line-height: 24px; } +form .row span { width: 600px; color: #666666; } + +.tip_box { display: block; height: 30px; position: absolute; z-index: 1000; } + +.tip_arrow { display: block; position: absolute; top: 5px; left: -11px; width: 12px; z-index: 700; } + +.tip_content { height: auto !important; height: 20px; min-height: 20px; padding: 10px 5px 5px 5px; border: 1px solid #666; background: #fff; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; z-index: 500; position: absolute; top: 0; left: -1px; width: 300px; } + +.tip_close { position: absolute; text-decoration: none; left: 100%; top: 0; margin-left: -12px; } + +#overlay { display: none; position: fixed; background: #000; z-index: 2000; } + +#loading { padding: 10px 10px 10px 60px; width: 300px; height: 100px; background: url('../images/ajax-loader.gif?1312925608') 10px 50% no-repeat white; position: fixed; display: none; z-index: 3000; } +#loading h4 { margin: 3px 0 0 0; padding: 0; color: #d80; } + +.tip { display: inline-block; width: 16px; height: 16px; outline: none; text-decoration: none; color: #d80; } + +#links { margin: 10px 0; } diff --git a/setup/images/.DS_Store b/setup/images/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0781e8368c16159126b95903f523919883384c09 Binary files /dev/null and b/setup/images/.DS_Store differ diff --git a/setup/images/ajax-loader.gif b/setup/images/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c2f7c058836808f7f1f5f9a4bdf37d4e5f9284a Binary files /dev/null and b/setup/images/ajax-loader.gif differ diff --git a/setup/images/background.jpg b/setup/images/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eebfa40b8f24cf1a088e6d4eb1a6b279e958ad7c Binary files /dev/null and b/setup/images/background.jpg differ diff --git a/setup/images/cog.png b/setup/images/cog.png new file mode 100755 index 0000000000000000000000000000000000000000..20171bc655e21cb8df4b800230f7b2867d2f0ca2 Binary files /dev/null and b/setup/images/cog.png differ diff --git a/setup/images/database.png b/setup/images/database.png new file mode 100755 index 0000000000000000000000000000000000000000..ffc935d50321c09757ee65be1c211f9ddd64d17f Binary files /dev/null and b/setup/images/database.png differ diff --git a/setup/images/email.png b/setup/images/email.png new file mode 100755 index 0000000000000000000000000000000000000000..7348aed77fe6a64c2210a202f12c6eccae7fcf24 Binary files /dev/null and b/setup/images/email.png differ diff --git a/setup/images/grey_btn_bg.png b/setup/images/grey_btn_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..3b7fca9d3c41fcefd6ca49d6aa98ea3fc3fe1a41 Binary files /dev/null and b/setup/images/grey_btn_bg.png differ diff --git a/setup/images/incomplete.png b/setup/images/incomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..a3dab9a929c59bf96d4d51fed5d17329da189bd8 Binary files /dev/null and b/setup/images/incomplete.png differ diff --git a/setup/images/lightbulb.png b/setup/images/lightbulb.png new file mode 100755 index 0000000000000000000000000000000000000000..117285ff7e4990a05278fa6eecdd654c611e819c Binary files /dev/null and b/setup/images/lightbulb.png differ diff --git a/setup/images/logo-upgrade.png b/setup/images/logo-upgrade.png new file mode 100644 index 0000000000000000000000000000000000000000..f8430d2488076b97f4ebd42d9e80d840e8f4773f Binary files /dev/null and b/setup/images/logo-upgrade.png differ diff --git a/setup/images/logo.png b/setup/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d34958ca04f08e78e5b67dfcce4c210183cb3255 Binary files /dev/null and b/setup/images/logo.png differ diff --git a/setup/images/no.png b/setup/images/no.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc50c6bc73a1a93754306483dd15aaa45e188dd Binary files /dev/null and b/setup/images/no.png differ diff --git a/setup/images/system.png b/setup/images/system.png new file mode 100755 index 0000000000000000000000000000000000000000..d07d5fde68c0b3681e67d220a7d11d72a64da487 Binary files /dev/null and b/setup/images/system.png differ diff --git a/setup/images/tip_arrow.png b/setup/images/tip_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..61a49c706eff3f23b690cfe50b0b39a816dd1fe9 Binary files /dev/null and b/setup/images/tip_arrow.png differ diff --git a/setup/images/user.png b/setup/images/user.png new file mode 100755 index 0000000000000000000000000000000000000000..4166dbfe0e703f436aa7539260136bc758770a82 Binary files /dev/null and b/setup/images/user.png differ diff --git a/setup/images/yes.png b/setup/images/yes.png new file mode 100644 index 0000000000000000000000000000000000000000..8ccfa4569a5fcdc4a9de556dba0c19a20d807233 Binary files /dev/null and b/setup/images/yes.png differ diff --git a/setup/inc/class.attachment.migrate.php b/setup/inc/class.attachment.migrate.php new file mode 100644 index 0000000000000000000000000000000000000000..20385fd936439fbe868c2069b4fde0601a80b0ac --- /dev/null +++ b/setup/inc/class.attachment.migrate.php @@ -0,0 +1,162 @@ +<?php +/********************************************************************* + class.attachment.migrate.php + + Attachment migration from file-based attachments in pre-1.7 to + database-backed attachments in osTicket v1.7. This class provides the + hardware to find and retrieve old attachments and move them into the new + database scheme with the data in the actual database. + + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.file.php'); + +class AttachmentMigrater { + function AttachmentMigrater() { + $this->queue = array(); + $this->current = 0; + $this->errors = 0; + $this->errorList = array(); + } + /** + * Identifies attachments in need of migration and queues them for + * migration to the new database schema. + * + * @see ::next() for output along the way + * + * Returns: + * TRUE/FALSE to indicate if migration finished without any errors + */ + function start_migration() { + $this->findAttachments(); + $this->total = count($this->queue); + } + /** + * Process the migration for a unit of time. This will be used to + * overcome the execution time restriction of PHP. This instance can be + * stashed in a session and have this method called periodically to + * process another batch of attachments + */ + function do_batch($max, $time=20) { + $start = time(); + $this->errors = 0; + while (count($this->queue) && $count++ < $max && time()-$start < $time) + $this->next(); + # TODO: Log success/error indication of migration of attachments + return (!$this->errors); + } + + function queue($fileinfo) { + $this->queue[] = $fileinfo; + } + function getQueueLength() { return count($this->queue); } + /** + * Processes the next item on the work queue. Emits a JSON messages to + * indicate current progress. + * + * Returns: + * TRUE/NULL if the migration was successful + */ + function next() { + # Fetch next item -- use the last item so the array indices don't + # need to be recalculated for every shift() operation. + $info = array_pop($this->queue); + # Attach file to the ticket + if (!($info['data'] = @file_get_contents($info['path']))) { + # Continue with next file + return $this->error( + sprintf('%s: Cannot read file contents', $info['path'])); + } + # Get the mime/type of each file + # XXX: Use finfo_buffer for PHP 5.3+ + $info['type'] = mime_content_type($info['path']); + if (!($fileId = AttachmentFile::save($info))) { + return $this->error( + sprintf('%s: Unable to migrate attachment', $info['path'])); + } + # Update the ATTACHMENT_TABLE record to set file_id + db_query('update '.TICKET_ATTACHMENT_TABLE + .' set file_id='.db_input($fileId) + .' where attach_id='.db_input($info['attachId'])); + # Remove disk image of the file. If this fails, the migration for + # this file would not be retried, because the file_id in the + # TICKET_ATTACHMENT_TABLE has a nonzero value now + if (!@unlink($info['path'])) + $this->error( + sprintf('%s: Unable to remove file from disk', + $info['path'])); + # TODO: Log an internal note to the ticket? + return true; + } + /** + * From (class Ticket::fixAttachments), used to detect the locations of + * attachment files + */ + /* static */ function findAttachments(){ + global $cfg; + + $res=db_query('SELECT attach_id, file_name, file_key, Ti.created' + .' FROM '.TICKET_ATTACHMENT_TABLE.' TA' + .' JOIN '.TICKET_TABLE.' Ti ON Ti.ticket_id=TA.ticket_id' + .' WHERE NOT file_id'); + if (!$res) { + return $this->error('Unable to query for attached files'); + } elseif (!db_num_rows($res)) { + return true; + } + $dir=$cfg->getUploadDir(); + while (list($id,$name,$key,$created)=db_fetch_row($res)) { + $month=date('my',strtotime($created)); + $info=array( + 'name'=> $name, + 'attachId'=> $id, + ); + $filename15=sprintf("%s/%s_%s",rtrim($dir,'/'),$key,$name); + $filename16=sprintf("%s/%s/%s_%s",rtrim($dir,'/'),$month,$key,$name); //new destination. + if (file_exists($filename15)) { + $info['path'] = $filename15; + } elseif (file_exists($filename16)) { + $info['path'] = $filename16; + } else { + # XXX Cannot find file for attachment + $this->error(sprintf('%s: Unable to locate attachment file', + $name)); + # No need to further process this file + continue; + } + # TODO: Get the size and mime/type of each file. + # + # NOTE: If filesize() fails and file_get_contents() doesn't, + # then the AttachmentFile::save() method will automatically + # estimate the filesize based on the length of the string data + # received in $info['data'] -- ie. no need to do that here. + # + # NOTE: The size is done here because it should be quick to + # lookup out of file inode already loaded. The mime/type may + # take a while because it will require a second IO to read the + # file data. To ensure this will finish before the + # max_execution_time, perform the type match in the ::next() + # method since the entire file content will be read there + # anyway. + $info['size'] = @filesize($info['path']); + # Coroutines would be nice .. + $this->queue($info); + } + } + + function error($what) { + $this->errors++; + $this->errorList[] = $what; + # Assist in returning FALSE for inline returns with this method + return false; + } + function getErrors() { + return $this->errorList; + } +} diff --git a/setup/inc/class.setup.php b/setup/inc/class.setup.php new file mode 100644 index 0000000000000000000000000000000000000000..2660da6a260cc8afd6360f3f6aec4ea5f4131bf7 --- /dev/null +++ b/setup/inc/class.setup.php @@ -0,0 +1,386 @@ +<?php +/********************************************************************* + class.setup.php + + osTicket setup wizard. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +Class SetupWizard { + + //Mimimum requirements + var $prereq = array('php' => '4.3', + 'mysql' => '4.4'); + + //Version info - same as the latest version. + var $version ='1.7-rc1'; + var $version_verbose='1.7 RC 1'; + + //Errors + var $errors=array(); + + function SetupWizard(){ + $this->errors=array(); + } + + function load_sql_file($file, $prefix, $debug=false) { + + if(!file_exists($file) || !($schema=file_get_contents($file))) + return $this->abort('Error accessing SQL file'); + + return $this->load_sql($schema, $prefix, $debug); + } + + /* + load SQL schema - assumes MySQL && existing connection + */ + function load_sql($schema, $prefix, $debug=false) { + + # Strip comments and remarks + $schema=preg_replace('%^\s*(#|--).*$%m','',$schema); + # Replace table prefis + $schema = str_replace('%TABLE_PREFIX%',$prefix, $schema); + # Split by semicolons - and cleanup + if(!($statements = array_filter(array_map('trim', @explode(';', $schema))))) + return $this->abort('Error parsing SQL schema'); + + + @mysql_query('SET SESSION SQL_MODE =""'); + foreach($statements as $k=>$sql) { + if(!mysql_query($sql)) { + if($debug) echo "[$sql]=>".mysql_error(); + return $this->abort("[$sql] - ".mysql_error()); + } + } + + return true; + } + + function getVersion() { + return $this->version; + } + + function getVersionVerbose() { + return $this->version_verbose; + } + + function getPHPVersion() { + return $this->prereq['php']; + } + + function getMySQLVersion() { + return $this->prereq['mysql']; + } + + function check_php() { + return (version_compare(PHP_VERSION,$this->getPHPVersion())>=0); + } + + function check_mysql() { + return (extension_loaded('mysql')); + } + + function check_prereq() { + return ($this->check_php() && $this->check_mysql()); + } + + /* + @error is a mixed var. + */ + function abort($error) { + + + if($error && is_array($error)) + $this->errors = array_merge($this->errors,$error); + elseif($error) + $this->errors[] = $error; + + //Always returns FALSE. + return false; + } + + function getErrors(){ + + return $this->errors; + } + + /* Access and user validation*/ + + function getThisUser() { + + + } +} + +class Upgrader extends SetupWizard { + + var $prefix; + + function Upgrader($prefix) { + $this->prefix = $prefix; + $this->errors = array(); + } + + function getTablePrefix() { + return $this->prefix; + } + + /* upgrade magic related to the given version */ + function upgradeTo($version) { + + $errors = array(); + switch($version) { + case '1.7-RC1': + //TODO: latest upgrade logic. + break; + case '1.6 ST': + //TODO: refactor code from 1.6 ST. + break; + case '1.6 RC5': + //TODO: refactor code from 1.6 ST. + break; + default: + //XXX: escape version + return $this->abort('Trying to upgrade unknown version '.$version); + } + + if($errors) + return $this->abort($errors); + + return true; + } + + /* + Do base upgrade + Does fall-through upgrade until we reach the current version. + We're assumming the user - is upgrading upgradable version of osTicket! + @version - version number to upgrade from! + */ + function upgradeFrom($version) { + + if(!$version || $this->getErrors()) + return false; + + if(!strcasecmp($version,$this->getVersion())) + return true; + + //XXX: Note FALLTHROUGH (we only break on error) and uppercase cases. + switch(strtoupper($version)) { + case 'OLD': //Upgrade old versions to 1.6 ST. + if(!$this->upgradeTo('1.6 RC5')) break; + /* FALLTHROUGH */ + case '1.6 RC5': //Upgrade 1.6 RC5 to 1.6 ST + if(!$this->upgradeTo('1.6 ST')) break; + /* FALLTHROUGH */ + case '1.6 ST': //Upgrade 1.6 ST to to 1.7 RC1 + if(!$this->upgradeTo('1.7-RC1')) break; + /* LAST CASE IS NOT FALLTHROUGH */ + break; + default: //Catch all - Upgrading older versions 1.3+ + return $this->upgradeFrom('OLD'); + } + //XXX: Set errors??? + + return (!$this->getErrors()); + } + + function cleanup() { + //FIXME: cleanup logic here. + sleep(2); + + return true; + } +} + +/* + Installer class - latest version. + */ +class Installer extends SetupWizard { + + var $config; + + function Installer($configfile) { + $this->config =$configfile; + $this->errors=array(); + } + + function getConfigFile() { + return $this->config; + } + + function config_exists() { + return ($this->getConfigFile() && file_exists($this->getConfigFile())); + } + + function config_writable() { + return ($this->getConfigFile() && is_writable($this->getConfigFile())); + } + + function check_config() { + return ($this->config_exists() && $this->config_writable()); + } + + //XXX: Latest version insall logic...no carry over. + function install($vars) { + + $this->errors=$f=array(); + + $f['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); + $f['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); + $f['fname'] = array('type'=>'string', 'required'=>1, 'error'=>'First name required'); + $f['lname'] = array('type'=>'string', 'required'=>1, 'error'=>'Last name required'); + $f['admin_email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); + $f['username'] = array('type'=>'username', 'required'=>1, 'error'=>'Username required'); + $f['passwd'] = array('type'=>'password', 'required'=>1, 'error'=>'Password required'); + $f['passwd2'] = array('type'=>'string', 'required'=>1, 'error'=>'Confirm password'); + $f['prefix'] = array('type'=>'string', 'required'=>1, 'error'=>'Table prefix required'); + $f['dbhost'] = array('type'=>'string', 'required'=>1, 'error'=>'Hostname required'); + $f['dbname'] = array('type'=>'string', 'required'=>1, 'error'=>'Database name required'); + $f['dbuser'] = array('type'=>'string', 'required'=>1, 'error'=>'Username required'); + $f['dbpass'] = array('type'=>'string', 'required'=>1, 'error'=>'password required'); + + + if(!Validator::process($f,$vars,$this->errors) && !$this->errors['err']) + $this->errors['err']='Missing or invalid data - correct the errors and try again.'; + + + //Staff's email can't be same as system emails. + if($vars['admin_email'] && $vars['email'] && !strcasecmp($vars['sysemail'],$vars['email'])) + $this->errors['admin_email']='Conflicts with system email above'; + //Admin's pass confirmation. + if(!$this->errors && strcasecmp($vars['passwd'],$vars['passwd2'])) + $this->errors['passwd2']='passwords to not match!'; + //Check table prefix underscore required at the end! + if($vars['prefix'] && substr($vars['prefix'], -1)!='_') + $this->errors['prefix']='Bad prefix. Must have underscore (_) at the end. e.g \'ost_\''; + + //Make sure admin username is not very predictable. XXX: feels dirty but necessary + if(!$this->errors['username'] && in_array(strtolower($vars['username']),array('admin','admins','username','osticket'))) + $this->errors['username']='Bad username'; + + //MYSQL: Connect to the DB and check the version & database (create database if it doesn't exist!) + if(!$this->errors) { + if(!db_connect($vars['dbhost'],$vars['dbuser'],$vars['dbpass'])) + $this->errors['db']='Unable to connect to MySQL server. Possibly invalid login info.'; + elseif(db_version()< $this->getMySQLVersion()) + $this->errors['db']=sprintf('osTicket requires MySQL %s or better!',$this->getMySQLVersion()); + elseif(!db_select_database($vars['dbname']) && !db_create_database($vars['dbname'])) { + $this->errors['dbname']='Database doesn\'t exist'; + $this->errors['db']='Unable to create the database.'; + } elseif(!db_select_database($vars['dbname'])) { + $this->errors['dbname']='Unable to select the database'; + } + } + + //bailout on errors. + if($this->errors) return false; + + /*************** We're ready to install ************************/ + define('ADMIN_EMAIL',$vars['admin_email']); //Needed to report SQL errors during install. + define('PREFIX',$vars['prefix']); //Table prefix + + $schemaFile =INC_DIR.'sql/osticket-v1.7-mysql.sql'; //DB dump. + $debug = true; //XXX:Change it to true to show SQL errors. + + //Last minute checks. + if(!file_exists($schemaFile)) + $this->errors['err']='Internal Error - please make sure your download is the latest (#1)'; + elseif(!file_exists($this->getConfigFile()) || !($configFile=file_get_contents($this->getConfigFile()))) + $this->errors['err']='Unable to read config file. Permission denied! (#2)'; + elseif(!($fp = @fopen($this->getConfigFile(),'r+'))) + $this->errors['err']='Unable to open config file for writing. Permission denied! (#3)'; + elseif(!$this->load_sql_file($schemaFile,$vars['prefix'],$debug)) + $this->errors['err']='Error parsing SQL schema! Get help from developers (#4)'; + + if(!$this->errors) { + //Create admin user. + $sql='INSERT INTO '.PREFIX.'staff SET created=NOW() ' + .', isactive=1, isadmin=1, group_id=1, dept_id=1, timezone_id=1 ' + .', email='.db_input($_POST['email']) + .', firstname='.db_input($vars['fname']) + .', lastname='.db_input($vars['lname']) + .', username='.db_input($vars['username']) + .', passwd='.db_input(Passwd::hash($vars['passwd'])); + if(!mysql_query($sql) || !($uid=mysql_insert_id())) + $this->errors['err']='Unable to create admin user (#6)'; + } + + if(!$this->errors) { + //Create config settings---default settings! + //XXX: rename ostversion helpdesk_* ?? + $sql='INSERT INTO '.PREFIX.'config SET updated=NOW(), isonline=0 ' + .', default_email_id=1, alert_email_id=2, default_dept_id=1 ' + .', default_sla_id=1, default_timezone_id=1, default_template_id=1 ' + .', admin_email='.db_input($vars['admin_email']) + .', schema_signature='.db_input(md5_file($schemaFile)) + .', helpdesk_url='.db_input(URL) + .', helpdesk_title='.db_input($vars['name']); + if(!mysql_query($sql) || !($cid=mysql_insert_id())) + $this->errors['err']='Unable to create config settings (#7)'; + } + + if($this->errors) return false; //Abort on internal errors. + + + //Rewrite the config file - MUST be done last to allow for installer recovery. + $configFile= str_replace("define('OSTINSTALLED',FALSE);","define('OSTINSTALLED',TRUE);",$configFile); + $configFile= str_replace('%ADMIN-EMAIL',$vars['admin_email'],$configFile); + $configFile= str_replace('%CONFIG-DBHOST',$vars['dbhost'],$configFile); + $configFile= str_replace('%CONFIG-DBNAME',$vars['dbname'],$configFile); + $configFile= str_replace('%CONFIG-DBUSER',$vars['dbuser'],$configFile); + $configFile= str_replace('%CONFIG-DBPASS',$vars['dbpass'],$configFile); + $configFile= str_replace('%CONFIG-PREFIX',$vars['prefix'],$configFile); + $configFile= str_replace('%CONFIG-SIRI',Misc::randcode(32),$configFile); + if(!$fp || !ftruncate($fp,0) || !fwrite($fp,$configFile)) { + $this->errors['err']='Unable to write to config file. Permission denied! (#5)'; + return false; + } + @fclose($fp); + + /************* Make the system happy ***********************/ + //Create default emails! + $email = $vars['email']; + list(,$domain)=explode('@',$vars['email']); + $sql='INSERT INTO '.PREFIX.'email (`email_id`, `dept_id`, `name`,`email`,`created`,`updated`) VALUES ' + ." (1,1,'Support','$email',NOW(),NOW())" + .",(2,1,'osTicket Alerts','alerts@$domain',NOW(),NOW())" + .",(3,1,'','noreply@$domain',NOW(),NOW())"; + @mysql_query($sql); + + //Create a ticket to make the system warm and happy. + $sql='INSERT INTO '.PREFIX.'ticket SET created=NOW(), status="open", source="Web", priority_id=2, dept_id=1 ' + .' ,ticketID='.db_input(Misc::randNumber(6)) + .' ,email="support@osticket.com" ' + .' ,name="osTicket Support" ' + .' ,subject="osTicket Installed!"'; + if(mysql_query($sql) && ($tid=mysql_insert_id())) { + if(!($msg=file_get_contents(INC_DIR.'msg/installed.txt'))) + $msg='Congratulations and Thank you for choosing osTicket!'; + + $sql='INSERT INTO '.PREFIX.'ticket_message SET created=NOW(),source="Web" ' + .', ticket_id='.db_input($tid) + .', message='.db_input($msg); + @mysql_query($sql); + } + //TODO: create another personalized ticket and assign to admin?? + + //Log a message. + $msg="Congratulations osTicket basic installation completed!\n\nThank you for choosing osTicket!"; + $sql='INSERT INTO '.PREFIX.'syslog SET created=NOW(),updated=NOW(),log_type="Debug" ' + .', title="osTicket installed!"' + .', log='.db_input($msg) + .', ip_address='.db_input($_SERVER['REMOTE_ADDR']); + @mysql_query($sql); + + return true; + } +} +?> diff --git a/setup/inc/file-missing.inc.php b/setup/inc/file-missing.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..2c84883c4b676011f868ffe2dd9adaf91ebd280e --- /dev/null +++ b/setup/inc/file-missing.inc.php @@ -0,0 +1,30 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +?> + <div id="main"> + <h1 style="color:#FF7700;">Configuration file missing!</h1> + <div id="intro"> + <p>osTicket installer requires ability to write to the configuration file, <b>include/ost-config.php</b>. A template copy is located in the include directory (<b>include/ost-sampleconfig.php</b>). + </p> + </div> + <h3>Solution: <font color="red"><?php echo $errors['err']; ?></font></h3> + Rename the sample file <b>include/ost-sampleconfig.php</b> to <b>ost-config.php</b> and click continue below. + <ul> + <li><b>CLI:</b><br><i>cp include/ost-sampleconfig.php include/ost-config.php</i></li> + <li><b>FTP:</b><br> </li> + <li><b>Cpanel:</b><br> </li> + </ul> + <p>If sample config file is missing - please make sure you uploaded all files in 'upload' folder or refer to the <a target="_blank" href="http://osticket.com/wiki/Installation">Installation Guide</a></p> + <div id="bar"> + <form method="post" action="install.php"> + <input type="hidden" name="s" value="config"> + <input class="btn" type="submit" name="submit" value="Continue »"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Need Help?</h3> + <p> + If you are looking for a greater level of support, we provide <u>professional installation services</u> and commercial support with guaranteed response times, and access to the core development team. We can also help customize osTicket or even add new features to the system to meet your unique needs. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a> + </p> + </div> diff --git a/setup/inc/file-perm.inc.php b/setup/inc/file-perm.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..7f93a302c266cabeb4a466e3f3951e14b508c90e --- /dev/null +++ b/setup/inc/file-perm.inc.php @@ -0,0 +1,32 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +?> + <div id="main"> + <h1 style="color:#FF7700;">Configuration file is not writable</h1> + <div id="intro"> + <p> + osTicket installer requires ability to write to the configuration file <b>include/ost-config.php</b>. + </p> + </div> + <h3>Solution: <font color="red"><?php echo $errors['err']; ?></font></h3> + Please follow the instructions below to give read and write access to the web server. + <ul> + <li><b>CLI</b>:<br><i>chmod 0777 include/ost-config.php</i></li> + <li><b>FTP</b>:<br>Using WS_FTP this would be right hand clicking on the fil, selecting chmod, and then giving all permissions to the file.</li> + <li><b>Cpanel</b>:<br>Click on the file, select change permission, and then giving all permissions to the file.</li> + </ul> + + <p><i>Don't worry! We'll remind you to take away the write access post-install</i>.</p> + <div id="bar"> + <form method="post" action="install.php"> + <input type="hidden" name="s" value="config"> + <input class="btn" type="submit" name="submit" value="Done? Continue »"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Need Help?</h3> + <p> + If you are looking for a greater level of support, we provide <u>professional installation services</u> and commercial support with guaranteed response times, and access to the core development team. We can also help customize osTicket or even add new features to the system to meet your unique needs. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a> + </p> + </div> diff --git a/setup/inc/file-unclean.inc.php b/setup/inc/file-unclean.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..1de4607e82bb5a2db8b7aea204fc6c57ba390c26 --- /dev/null +++ b/setup/inc/file-unclean.inc.php @@ -0,0 +1,18 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +?> + <div id="main"> + <h1 style="color:#FF7700;">osTicket is already installed?</h1> + <div id="intro"> + <p>Configuration file already changed - which could mean osTicket is already installed or the config file is currupted. If you are trying to upgrade osTicket, then <a href="upgrade.php" >click here</a>.</p> + + <p>If you believe this is in error, please try replacing the config file with a unchanged template copy and try again or get technical help.</p> + <p>Refer to the <a target="_blank" href="http://osticket.com/wiki/Installation">Installation Guide</a> on the wiki for more information.</p> + </div> + </div> + <div id="sidebar"> + <h3>Need Help?</h3> + <p> + We provide <u>professional installation services</u> and commercial support with guaranteed response times, and access to the core development team. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a> + </p> + </div> diff --git a/setup/inc/footer.inc.php b/setup/inc/footer.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..90dc3ddc75ffb5e5928a943f2ff6968d67b6499c --- /dev/null +++ b/setup/inc/footer.inc.php @@ -0,0 +1,7 @@ + + <div class="clear"></div> + </div> <!-- content --> + </div> <!-- wizard --> + <div id="footer" class="centered">Copyright © 2012 <a target="_blank" href="http://osticket.com">osTicket.com</a></div> +</body> +</html> diff --git a/setup/inc/header.inc.php b/setup/inc/header.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..977050bc58441041b3036386aa634b804e568d6c --- /dev/null +++ b/setup/inc/header.inc.php @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<head> + <title><?php echo $wizard['title']; ?></title> + <link rel="stylesheet" href="css/wizard.css"> + <script type="text/javascript" src="js/jquery-1.6.2.min.js"></script> + <script type="text/javascript" src="js/tips.js"></script> + <script type="text/javascript" src="js/setup.js"></script> +</head> +<body> + <div id="wizard"> + <div id="header"> + <img id="logo" src="./images/<?php echo $wizard['logo']?$wizard['logo']:'logo.png'; ?>" width="280" height="72" alt="osTicket"> + <ul> + <li class="info"><?php echo $wizard['tagline']; ?></li> + <li> + <?php + foreach($wizard['menu'] as $k=>$v) + echo sprintf('<a target="_blank" href="%s">%s</a> — ',$v,$k); + ?> + <a target="_blank" href="http://osticket.com/support/contact.php">Contact Us</a> + </li> + </ul> + </div> + <div id="content"> diff --git a/setup/inc/install-done.inc.php b/setup/inc/install-done.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..3aaf5caf33712eb6f2157966f609038a2fe99dd3 --- /dev/null +++ b/setup/inc/install-done.inc.php @@ -0,0 +1,48 @@ +<?php if(!defined('SETUPINC')) die('Kwaheri!'); +$url=URL; + +?> + <div id="main"> + <h1 style="color:green;">Congratulations!</h1> + <div id="intro"> + <p>Your osTicket installation has been completed successfully. Your next step is to fully configure your new support ticket system for use, but before you get to it please take a minute to cleanup.</p> + + <h2>Config file permission:</h2> + Change permission of ost-config.php to remove write access as shown below. + <ul> + <li><b>CLI</b>:<br><i>chmod 0664 include/ost-config.php</i></li> + <li><b>FTP</b>:<br>Using WS_FTP this would be right hand clicking on the file, selecting chmod, and then remove write access</li> + <li><b>Cpanel</b>:<br>Click on the file, select change permission, and then remove write access.</li> + </ul> + </div> + <p>Below, you'll find some useful links regarding your installation.</p> + <table border="0" cellspacing="0" cellpadding="5" width="580" id="links"> + <tr> + <td width="50%"> + <strong>Your osTicket URL:</strong><Br> + <a href="<?php echo $url; ?>"><?php echo $url; ?></a> + </td> + <td width="50%"> + <strong>Your Staff Control Panel:</strong><Br> + <a href="../scp/admin.php"><?php echo $url; ?>scp</a> + </td> + </tr> + <tr> + <td width="50%"> + <strong>osTicket Forums:</strong><Br> + <a href="#">http://osticket.com/forums/</a> + </td> + <td width="50%"> + <strong>osTicket Community Wiki:</strong><Br> + <a href="#">http://osticket.com/wiki/</a> + </td> + </tr> + </table> + <p><b>PS</b>: Don't just make customers happy, make happy customers!</p> + </div> + <div id="sidebar"> + <h3>What's Next?</h3> + <p><b>Post-Install Setup</b>: You can now log in to <a href="../scp/admin.php" target="_blank">Admin Panel</a> with the username and password you created during the install process. After a successful log in, you can proceed with post-install setup. For complete and upto date guide see <a href="http://osticket.com/wiki/Post-Install_Setup_Guide" target="_blank">osTicket wiki</a></p> + + <p><b>Commercial Support Available</b>: Don't let technical problems impact your osTicket implementation. Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely. <a target="_blank" href="http://osticket.com/support/commercial_support.php.php">Learn More!</a></p> + </div> diff --git a/setup/inc/install-prereq.inc.php b/setup/inc/install-prereq.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..b07ffd996c5f231c18c7471d419d7ec62b55969b --- /dev/null +++ b/setup/inc/install-prereq.inc.php @@ -0,0 +1,41 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); + +?> + + <div id="main"> + <h1>Thank You for Choosing osTicket!</h1> + <div id="intro"> + <p>We are delighted you have chosen osTicket for your customer support ticketing system!</p> + <p>The installer will guide you every step of the way in the installation process. You're minutes away from your awesome customer support system!</p> + </div> + <h2>Prerequisites.</h3> + <p>Before we begin, we'll check your server configuration to make sure you meet the minimum requirements to install and run osTicket.</p> + <h3>Required: <font color="red"><?php echo $errors['prereq']; ?></font></h3> + These items are necessary in order to install and use osTicket. + <ul class="progress"> + <li class="<? echo $installer->check_php()?'yes':'no'; ?>"> + PHP v4.3 or greater - (<small><b><?php echo PHP_VERSION; ?></b></small>)</li> + <li class="<? echo $installer->check_mysql()?'yes':'no'; ?>"> + MySQL v4.4 or greater - (<small><b><?php echo extension_loaded('mysql')?'module loaded':'missing!'; ?></b></small>)</li> + </ul> + <h3>Recommended:</h3> + You can use osTicket without these, but you may not be able to use all features. + <ul class="progress"> + <li class="<? echo extension_loaded('mcrypt')?'yes':'no'; ?>">Mcrypt extension</li> + <li class="<? echo extension_loaded('gd')?'yes':'no'; ?>">Gdlib extension</li> + <li class="<? echo extension_loaded('imap')?'yes':'no'; ?>">PHP IMAP extension</li> + </ul> + <div id="bar"> + <form method="post" action="install.php"> + <input type="hidden" name="s" value="prereq"> + <input class="btn" type="submit" name="submit" value="Continue »"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Need Help?</h3> + <p> + If you are looking for a greater level of support, we provide <u>professional installation services</u> and commercial support with guaranteed response times, and access to the core development team. We can also help customize osTicket or even add new features to the system to meet your unique needs. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a> + </p> + </div> diff --git a/setup/inc/install.inc.php b/setup/inc/install.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..5daefe123386f17458fe8afacf63d570b77cdca1 --- /dev/null +++ b/setup/inc/install.inc.php @@ -0,0 +1,120 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +$info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbhost'=>'localhost'); + +//XXX: Remove b4 release. +if($_SESSION['installer']['info'] && !$_POST) + $info=$_SESSION['installer']['info']; + + +?> +<div id="main" class="step2"> + <h1>osTicket Basic Installation</h1> + <p>Please fill out the information below to continue your osTicket installation. All fields are required.</p> + <font class="error"><strong><?php echo $errors['err']; ?></strong></font> + <form action="install.php" method="post" id="install"> + <input type="hidden" name="s" value="install"> + <h4 class="head system">System Settings</h4> + <span class="subhead">The URL of your helpdesk, its name, and the default system email address</span> + <div class="row"> + <label>Helpdesk URL:</label> + <span><strong><?php echo URL; ?></strong></span> + </div> + <div class="row"> + <label>Helpdesk Name:</label> + <input type="text" name="name" size="30" tabindex="1" value="<?php echo $info['name']; ?>"> + <a class="tip" href="#t1">?</a> + <font class="error"><?php echo $errors['name']; ?></font> + </div> + <div class="row"> + <label>Default Email:</label> + <input type="text" name="email" size="30" tabindex="2" value="<?php echo $info['email']; ?>"> + <a class="tip" href="#t2">?</a> + <font class="error"><?php echo $errors['email']; ?></font> + </div> + + <h4 class="head admin">Admin User</h4> + <span class="subhead">Your primary administrator account - you can add more users later.</span> + <div class="row"> + <label>First Name:</label> + <input type="text" name="fname" size="30" tabindex="3" value="<?php echo $info['fname']; ?>"> + <a class="tip" href="#t3">?</a> + <font class="error"><?php echo $errors['fname']; ?></font> + </div> + <div class="row"> + <label>Last Name:</label> + <input type="text" name="lname" size="30" tabindex="4" value="<?php echo $info['lname']; ?>"> + <a class="tip" href="#t4">?</a> + <font class="error"><?php echo $errors['lname']; ?></font> + </div> + <div class="row"> + <label>Email Address:</label> + <input type="text" name="admin_email" size="30" tabindex="5" value="<?php echo $info['admin_email']; ?>"> + <a class="tip" href="#t5">?</a> + <font class="error"><?php echo $errors['admin_email']; ?></font> + </div> + <div class="row"> + <label>Username:</label> + <input type="text" name="username" size="30" tabindex="6" value="<?php echo $info['username']; ?>" autocomplete="off"> + <a class="tip" href="#t6">?</a> + <font class="error"><?php echo $errors['username']; ?></font> + </div> + <div class="row"> + <label> Password:</label> + <input type="password" name="passwd" size="30" tabindex="7" value="<?php echo $info['passwd']; ?>" autocomplete="off"> + <a class="tip" href="#t7">?</a> + <font class="error"><?php echo $errors['passwd']; ?></font> + </div> + <div class="row"> + <label>Retype Password:</label> + <input type="password" name="passwd2" size="30" tabindex="8" value="<?php echo $info['passwd2']; ?>"> + <a class="tip" href="#t8">?</a> + <font class="error"><?php echo $errors['passwd2']; ?></font> + </div> + + <h4 class="head database">Database Settings</h4> + <span class="subhead">Database connection information <font class="error"><?php echo $errors['db']; ?></font></span> + <div class="row"> + <label>MySQL Table Prefix:</label> + <input type="text" name="prefix" size="30" tabindex="9" value="<?php echo $info['prefix']; ?>"> + <a class="tip" href="#t9">?</a> + <font class="error"><?php echo $errors['prefix']; ?></font> + </div> + <div class="row"> + <label>MySQL Hostname:</label> + <input type="text" name="dbhost" size="30" tabindex="10" value="<?php echo $info['dbhost']; ?>"> + <a class="tip" href="#t10">?</a> + <font class="error"><?php echo $errors['dbhost']; ?></font> + </div> + <div class="row"> + <label>MySQL Database:</label> + <input type="text" name="dbname" size="30" tabindex="11" value="<?php echo $info['dbname']; ?>"> + <a class="tip" href="#t11">?</a> + <font class="error"><?php echo $errors['dbname']; ?></font> + </div> + <div class="row"> + <label>MySQL Username:</label> + <input type="text" name="dbuser" size="30" tabindex="12" value="<?php echo $info['dbuser']; ?>"> + <a class="tip" href="#t12">?</a> + <font class="error"><?php echo $errors['dbuser']; ?></font> + </div> + <div class="row"> + <label>MySQL Password:</label> + <input type="password" name="dbpass" size="30" tabindex="13" value="<?php echo $info['dbpass']; ?>"> + <a class="tip" href="#t13">?</a> + <font class="error"><?php echo $errors['dbpass']; ?></font> + </div> + <br> + <div id="bar"> + <input class="btn" type="submit" value="Install Now" tabindex="14"> + </div> + </form> + </div> + <div> + <p><strong>Need Help?</strong> We provide <u>professional installation services</u> and commercial support. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a></p> + </div> + <div id="overlay"></div> + <div id="loading"> + <h4>Doing stuff!</h4> + Please wait... while we install your new support ticket system! + </div> diff --git a/setup/inc/msg/installed.txt b/setup/inc/msg/installed.txt new file mode 100644 index 0000000000000000000000000000000000000000..58bbeb5a0c6238df52c83fa91eeb701cdd00dbd5 --- /dev/null +++ b/setup/inc/msg/installed.txt @@ -0,0 +1,13 @@ +Thank you for choosing osTicket. + +Please make sure you join the osTicket forums at http://osticket.com/forums to stay up to date on the latest news, security alerts and updates. The osTicket forums are also a great place to get assistance, guidance, tips, and help from other osTicket users. In addition to the forums, the osTicket wiki provides a useful collection of educational materials, documentation, and notes from the community. We welcome your contributions to the osTicket community. + +If you are looking for a greater level of support, we provide professional services and commercial support with guaranteed response times, and access to the core development team. We can also help customize osTicket or even add new features to the system to meet your unique needs. + +Cheers, + +- +osTicket Team +http://osticket.com/ + +PS. Don't just make customers happy, make happy customers! diff --git a/setup/inc/msg/upgraded.txt b/setup/inc/msg/upgraded.txt new file mode 100644 index 0000000000000000000000000000000000000000..d88a29733659f1e9696a47338b89f7b7ccec1b97 --- /dev/null +++ b/setup/inc/msg/upgraded.txt @@ -0,0 +1,10 @@ + +osTicket upgraded successfully! Please refer to the Release Notes (http://osticket.com/wiki/Release_Notes) for more information about changes and new features. + +Be sure to join osTicket forum (http://osticket.com/forums) and our mailing list (http://osticket.com/support/subscribe.php) , if you haven't done so already, to stay up to date on announcements, security updates and alerts! Your contribution to osTicket community will be appreciated! + +The osTicket team is committed to providing support to all users through our free online resources and a full range of commercial support packages and services. For more information, or to discuss your needs, please contact us today at http://osticket.com/support/. Any feedback will be appreciated! + +- +osTicket Team +http://osticket.com/ diff --git a/setup/inc/ost-sampleconfig.php b/setup/inc/ost-sampleconfig.php new file mode 100644 index 0000000000000000000000000000000000000000..8a1f3b98eed8b51f6cbd535a8c181bebb5b7f312 --- /dev/null +++ b/setup/inc/ost-sampleconfig.php @@ -0,0 +1,47 @@ +<?php +/********************************************************************* + ost-config.php + + Static osTicket configuration file. Mainly useful for mysql login info. + Created during installation process and shouldn't change even on upgrades. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2010 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: + $Id: $ +**********************************************************************/ + +#Disable direct access. +if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__)) || !defined('ROOT_PATH')) die('kwaheri rafiki!'); + +#Install flag +define('OSTINSTALLED',FALSE); +if(OSTINSTALLED!=TRUE){ + if(!file_exists(ROOT_PATH.'setup/install.php')) die('Error: Contact system admin.'); //Something is really wrong! + //Invoke the installer. + header('Location: '.ROOT_PATH.'setup/install.php'); + exit; +} + +# Encrypt/Decrypt secret key - randomly generated during installation. +define('SECRET_SALT','%CONFIG-SIRI'); + +#Default admin email. Used only on db connection issues and related alerts. +define('ADMIN_EMAIL','%ADMIN-EMAIL'); + +#Mysql Login info +define('DBTYPE','mysql'); +define('DBHOST','%CONFIG-DBHOST'); +define('DBNAME','%CONFIG-DBNAME'); +define('DBUSER','%CONFIG-DBUSER'); +define('DBPASS','%CONFIG-DBPASS'); + +#Table prefix +define('TABLE_PREFIX','%CONFIG-PREFIX'); + +?> diff --git a/setup/inc/sql/osticket-v1.6.sql b/setup/inc/sql/osticket-v1.6.sql new file mode 100644 index 0000000000000000000000000000000000000000..7dcf180bb27f53f9a1b1a8cccae94604848140d0 --- /dev/null +++ b/setup/inc/sql/osticket-v1.6.sql @@ -0,0 +1,509 @@ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%api_key`; +CREATE TABLE `%TABLE_PREFIX%api_key` ( + `id` int(10) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) NOT NULL default '1', + `ipaddr` varchar(16) NOT NULL, + `apikey` varchar(255) NOT NULL, + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + UNIQUE KEY `ipaddr` (`ipaddr`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%api_key` (`id`, `isactive`, `ipaddr`, `apikey`, `updated`, `created`) VALUES (1, 1, '192.168.1.5', 'siri!', NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%config`; +CREATE TABLE `%TABLE_PREFIX%config` ( + `id` tinyint(1) unsigned NOT NULL auto_increment, + `isonline` tinyint(1) unsigned NOT NULL default '0', + `timezone_offset` float(3,1) NOT NULL default '0.0', + `enable_daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `staff_ip_binding` tinyint(1) unsigned NOT NULL default '1', + `staff_max_logins` tinyint(3) unsigned NOT NULL default '4', + `staff_login_timeout` int(10) unsigned NOT NULL default '2', + `staff_session_timeout` int(10) unsigned NOT NULL default '30', + `client_max_logins` tinyint(3) unsigned NOT NULL default '4', + `client_login_timeout` int(10) unsigned NOT NULL default '2', + `client_session_timeout` int(10) unsigned NOT NULL default '30', + `max_page_size` tinyint(3) unsigned NOT NULL default '25', + `max_open_tickets` tinyint(3) unsigned NOT NULL default '0', + `max_file_size` int(11) unsigned NOT NULL default '1048576', + `autolock_minutes` tinyint(3) unsigned NOT NULL default '3', + `overdue_grace_period` int(10) unsigned NOT NULL default '0', + `alert_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_dept_id` tinyint(3) unsigned NOT NULL default '0', + `default_priority_id` tinyint(2) unsigned NOT NULL default '2', + `default_template_id` tinyint(4) unsigned NOT NULL default '1', + `default_smtp_id` tinyint(4) unsigned NOT NULL default '0', + `spoof_default_smtp` tinyint(1) unsigned NOT NULL default '0', + `clickable_urls` tinyint(1) unsigned NOT NULL default '1', + `allow_priority_change` tinyint(1) unsigned NOT NULL default '0', + `use_email_priority` tinyint(1) unsigned NOT NULL default '0', + `enable_captcha` tinyint(1) unsigned NOT NULL default '0', + `enable_auto_cron` tinyint(1) unsigned NOT NULL default '0', + `enable_mail_fetch` tinyint(1) unsigned NOT NULL default '0', + `enable_email_piping` tinyint(1) unsigned NOT NULL default '0', + `send_sql_errors` tinyint(1) unsigned NOT NULL default '1', + `send_mailparse_errors` tinyint(1) unsigned NOT NULL default '1', + `send_login_errors` tinyint(1) unsigned NOT NULL default '1', + `save_email_headers` tinyint(1) unsigned NOT NULL default '1', + `strip_quoted_reply` tinyint(1) unsigned NOT NULL default '1', + `log_ticket_activity` tinyint(1) unsigned NOT NULL default '1', + `ticket_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `message_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `ticket_notice_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_admin` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `message_alert_active` tinyint(1) unsigned NOT NULL default '0', + `message_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `message_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `message_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `note_alert_active` tinyint(1) unsigned NOT NULL default '0', + `note_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `note_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `note_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_active` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `auto_assign_reopened_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0', + `show_answered_tickets` tinyint(1) NOT NULL default '0', + `hide_staff_name` tinyint(1) unsigned NOT NULL default '0', + `overlimit_notice_active` tinyint(1) unsigned NOT NULL default '0', + `email_attachments` tinyint(1) unsigned NOT NULL default '1', + `allow_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_email_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments_onlogin` tinyint(1) unsigned NOT NULL default '0', + `random_ticket_ids` tinyint(1) unsigned NOT NULL default '1', + `log_level` tinyint(1) unsigned NOT NULL default '2', + `log_graceperiod` int(10) unsigned NOT NULL default '12', + `upload_dir` varchar(255) NOT NULL default '', + `allowed_filetypes` varchar(255) NOT NULL default '.doc, .pdf', + `time_format` varchar(32) NOT NULL default ' h:i A', + `date_format` varchar(32) NOT NULL default 'm/d/Y', + `datetime_format` varchar(60) NOT NULL default 'm/d/Y g:i a', + `daydatetime_format` varchar(60) NOT NULL default 'D, M j Y g:ia', + `reply_separator` varchar(60) NOT NULL default '-- do not edit --', + `admin_email` varchar(125) NOT NULL default '', + `helpdesk_title` varchar(255) NOT NULL default 'osTicket Support Ticket System', + `helpdesk_url` varchar(255) NOT NULL default '', + `api_passphrase` varchar(125) NOT NULL default '', + `ostversion` varchar(16) NOT NULL default '', + `updated` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `isoffline` (`isonline`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; +CREATE TABLE `%TABLE_PREFIX%department` ( + `dept_id` int(11) unsigned NOT NULL auto_increment, + `tpl_id` int(10) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `autoresp_email_id` int(10) unsigned NOT NULL default '0', + `manager_id` int(10) unsigned NOT NULL default '0', + `dept_name` varchar(32) NOT NULL default '', + `dept_signature` tinytext NOT NULL, + `ispublic` tinyint(1) unsigned NOT NULL default '1', + `ticket_auto_response` tinyint(1) NOT NULL default '1', + `message_auto_response` tinyint(1) NOT NULL default '0', + `can_append_signature` tinyint(1) NOT NULL default '1', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`dept_id`), + UNIQUE KEY `dept_name` (`dept_name`), + KEY `manager_id` (`manager_id`), + KEY `autoresp_email_id` (`autoresp_email_id`), + KEY `tpl_id` (`tpl_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%department` (`dept_id`, `tpl_id`, `email_id`, `autoresp_email_id`, `manager_id`, `dept_name`, `dept_signature`, `ispublic`, `ticket_auto_response`, `message_auto_response`, `can_append_signature`, `updated`, `created`) VALUES +(1, 0, 1, 0, 0, 'Support', 'Support Dept', 1, 1, 1, 1, NOW(), NOW()), +(2, 0, 1, 0, 0, 'Billing', 'Billing Dept', 1, 1, 1, 1, NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email`; +CREATE TABLE `%TABLE_PREFIX%email` ( + `email_id` int(11) unsigned NOT NULL auto_increment, + `noautoresp` tinyint(1) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '2', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `email` varchar(125) NOT NULL default '', + `name` varchar(32) NOT NULL default '', + `userid` varchar(125) NOT NULL, + `userpass` varchar(125) NOT NULL, + `mail_active` tinyint(1) NOT NULL default '0', + `mail_host` varchar(125) NOT NULL, + `mail_protocol` enum('POP','IMAP') NOT NULL default 'POP', + `mail_encryption` enum('NONE','SSL') NOT NULL, + `mail_port` int(6) default NULL, + `mail_fetchfreq` tinyint(3) NOT NULL default '5', + `mail_fetchmax` tinyint(4) NOT NULL default '30', + `mail_delete` tinyint(1) NOT NULL default '0', + `mail_errors` tinyint(3) NOT NULL default '0', + `mail_lasterror` datetime default NULL, + `mail_lastfetch` datetime default NULL, + `smtp_active` tinyint(1) default '0', + `smtp_host` varchar(125) NOT NULL, + `smtp_port` int(6) default NULL, + `smtp_secure` tinyint(1) NOT NULL default '1', + `smtp_auth` tinyint(1) NOT NULL default '1', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`email_id`), + UNIQUE KEY `email` (`email`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_banlist`; +CREATE TABLE `%TABLE_PREFIX%email_banlist` ( + `id` int(11) NOT NULL auto_increment, + `email` varchar(255) NOT NULL default '', + `submitter` varchar(126) NOT NULL default '', + `added` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%email_banlist` (`id`, `email`, `submitter`, `added`) VALUES +(1, 'test@example.com', 'System', NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_template`; +CREATE TABLE `%TABLE_PREFIX%email_template` ( + `tpl_id` int(11) NOT NULL auto_increment, + `cfg_id` int(10) unsigned NOT NULL default '0', + `name` varchar(32) NOT NULL default '', + `notes` text, + `ticket_autoresp_subj` varchar(255) NOT NULL default '', + `ticket_autoresp_body` text NOT NULL, + `ticket_notice_subj` varchar(255) NOT NULL, + `ticket_notice_body` text NOT NULL, + `ticket_alert_subj` varchar(255) NOT NULL default '', + `ticket_alert_body` text NOT NULL, + `message_autoresp_subj` varchar(255) NOT NULL default '', + `message_autoresp_body` text NOT NULL, + `message_alert_subj` varchar(255) NOT NULL default '', + `message_alert_body` text NOT NULL, + `note_alert_subj` varchar(255) NOT NULL, + `note_alert_body` text NOT NULL, + `assigned_alert_subj` varchar(255) NOT NULL default '', + `assigned_alert_body` text NOT NULL, + `ticket_overdue_subj` varchar(255) NOT NULL default '', + `ticket_overdue_body` text NOT NULL, + `ticket_overlimit_subj` varchar(255) NOT NULL default '', + `ticket_overlimit_body` text NOT NULL, + `ticket_reply_subj` varchar(255) NOT NULL default '', + `ticket_reply_body` text NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`tpl_id`), + KEY `cfg_id` (`cfg_id`), + FULLTEXT KEY `message_subj` (`ticket_reply_subj`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +INSERT INTO `%TABLE_PREFIX%email_template` (`tpl_id`, `cfg_id`, `name`, `notes`, `ticket_autoresp_subj`, `ticket_autoresp_body`, `ticket_notice_subj`, `ticket_notice_body`, `ticket_alert_subj`, `ticket_alert_body`, `message_autoresp_subj`, `message_autoresp_body`, `message_alert_subj`, `message_alert_body`, `note_alert_subj`, `note_alert_body`, `assigned_alert_subj`, `assigned_alert_body`, `ticket_overdue_subj`, `ticket_overdue_body`, `ticket_overlimit_subj`, `ticket_overlimit_body`, `ticket_reply_subj`, `ticket_reply_body`, `created`, `updated`) VALUES +(1, 1, 'osTicket Default Template', 'Default osTicket templates', 'Support Ticket Opened [#%ticket]', '%name,\r\n\r\nA request for support has been created and assigned ticket #%ticket. A representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\nIf you wish to send additional comments or information regarding this issue, please don''t open a new ticket. Simply login using the link above and update the ticket.\r\n\r\n%signature', '[#%ticket] %subject', '%name,\r\n\r\nOur customer care team has created a ticket, #%ticket on your behalf, with the following message.\r\n\r\n%message\r\n\r\nIf you wish to provide additional comments or information regarding this issue, please don''t open a new ticket. You can update or view this ticket''s progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\n%signature', 'New Ticket Alert', '%staff,\r\n\r\nNew ticket #%ticket created.\r\n-------------------\r\nName: %name\r\nEmail: %email\r\nDept: %dept\r\n\r\n%message\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', '[#%ticket] Message Added', '%name,\r\n\r\nYour reply to support request #%ticket has been noted.\r\n\r\nYou can view this support request progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\n%signature', 'New Message Alert', '%staff,\r\n\r\nNew message appended to ticket #%ticket\r\n\r\n----------------------\r\nName: %name\r\nEmail: %email\r\nDept: %dept\r\n\r\n%message\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'New Internal Note Alert', '%staff,\r\n\r\nInternal note appended to ticket #%ticket\r\n\r\n----------------------\r\nName: %name\r\n\r\n%note\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Ticket #%ticket Assigned to you', '%assignee,\r\n\r\n%assigner has assigned ticket #%ticket to you!\r\n\r\n%message\r\n\r\nTo view complete details, simply login to the support system.\r\n\r\n- Your friendly Support Ticket System - powered by osTicket.', 'Stale Ticket Alert', '%staff,\r\n\r\nA ticket, #%ticket assigned to you or in your department is seriously overdue.\r\n\r\n%url/scp/tickets.php?id=%id\r\n\r\nWe should all work hard to guarantee that all tickets are being addressed in a timely manner. Enough baby talk...please address the issue or you will hear from me again.\r\n\r\n\r\n- Your friendly (although with limited patience) Support Ticket System - powered by osTicket.', 'Support Ticket Denied', '%name\r\n\r\nNo support ticket has been created. You''ve exceeded maximum number of open tickets allowed.\r\n\r\nThis is a temporary block. To be able to open another ticket, one of your pending tickets must be closed. To update or add comments to an open ticket simply login using the link below.\r\n\r\n%url/view.php?e=%email\r\n\r\nThank you.\r\n\r\nSupport Ticket System', '[#%ticket] %subject', '%name,\r\n\r\nA customer support staff member has replied to your support request, #%ticket with the following response:\r\n\r\n%response\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not send another email. Instead, reply to this email or login to your account for a complete archive of all your support requests and responses.\r\n\r\n%url/view.php?e=%email&t=%ticket\r\n\r\n%signature', NOW(), NOW()); + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`; +CREATE TABLE `%TABLE_PREFIX%groups` ( + `group_id` int(10) unsigned NOT NULL auto_increment, + `group_enabled` tinyint(1) unsigned NOT NULL default '1', + `group_name` varchar(50) NOT NULL default '', + `dept_access` varchar(255) NOT NULL default '', + `can_create_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_edit_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_delete_tickets` tinyint(1) unsigned NOT NULL default '0', + `can_close_tickets` tinyint(1) unsigned NOT NULL default '0', + `can_transfer_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_ban_emails` tinyint(1) unsigned NOT NULL default '0', + `can_manage_kb` tinyint(1) unsigned NOT NULL default '0', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`group_id`), + KEY `group_active` (`group_enabled`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%groups` (`group_id`, `group_enabled`, `group_name`, `dept_access`, `can_create_tickets`, `can_edit_tickets`, `can_delete_tickets`, `can_close_tickets`, `can_transfer_tickets`, `can_ban_emails`, `can_manage_kb`, `created`, `updated`) VALUES +(1, 1, 'Admins', '1', 1, 1, 1, 1, 1, 1, 1, NOW(), NOW()), +(2, 1, 'Managers', '1', 1, 1, 0, 1, 1, 1, 1, NOW(),NOW()), +(3, 1, 'Staff', '1', 1, 0, 0, 0, 0, 0, 0, NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic`; +CREATE TABLE `%TABLE_PREFIX%help_topic` ( + `topic_id` int(11) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) unsigned NOT NULL default '1', + `noautoresp` tinyint(3) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '0', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `topic` varchar(32) NOT NULL default '', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`topic_id`), + UNIQUE KEY `topic` (`topic`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +INSERT INTO `%TABLE_PREFIX%help_topic` (`topic_id`, `isactive`, `noautoresp`, `priority_id`, `dept_id`, `topic`, `created`, `updated`) VALUES +(1, 1, 0, 2, 1, 'Support', NOW(), NOW()), +(2, 1, 0, 3, 1, 'Billing', NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%kb_premade`; +CREATE TABLE `%TABLE_PREFIX%kb_premade` ( + `premade_id` int(10) unsigned NOT NULL auto_increment, + `dept_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `title` varchar(125) NOT NULL default '', + `answer` text NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`premade_id`), + UNIQUE KEY `title_2` (`title`), + KEY `dept_id` (`dept_id`), + KEY `active` (`isenabled`), + FULLTEXT KEY `title` (`title`,`answer`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%kb_premade` (`premade_id`, `dept_id`, `isenabled`, `title`, `answer`, `created`, `updated`) VALUES +(1, 0, 1, 'What is osTicket (sample)?', '\r\nosTicket is a widely-used open source support ticket system, an attractive alternative to higher-cost and complex customer support systems - simple, lightweight, reliable, open source, web-based and easy to setup and use.', NOW(), NOW()), +(2, 0, 1, 'Sample (with variables)', '\r\n%name,\r\n\r\nYour ticket #%ticket created on %createdate is in %dept department.\r\n\r\n', NOW(), NOW()); + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%staff`; +CREATE TABLE `%TABLE_PREFIX%staff` ( + `staff_id` int(11) unsigned NOT NULL auto_increment, + `group_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `username` varchar(32) NOT NULL default '', + `firstname` varchar(32) default NULL, + `lastname` varchar(32) default NULL, + `passwd` varchar(128) default NULL, + `email` varchar(128) default NULL, + `phone` varchar(24) NOT NULL default '', + `phone_ext` varchar(6) default NULL, + `mobile` varchar(24) NOT NULL default '', + `signature` tinytext NOT NULL, + `isactive` tinyint(1) NOT NULL default '1', + `isadmin` tinyint(1) NOT NULL default '0', + `isvisible` tinyint(1) unsigned NOT NULL default '1', + `onvacation` tinyint(1) unsigned NOT NULL default '0', + `daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `append_signature` tinyint(1) unsigned NOT NULL default '0', + `change_passwd` tinyint(1) unsigned NOT NULL default '0', + `timezone_offset` float(3,1) NOT NULL default '0.0', + `max_page_size` int(11) unsigned NOT NULL default '0', + `auto_refresh_rate` int(10) unsigned NOT NULL default '0', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `lastlogin` datetime default NULL, + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`staff_id`), + UNIQUE KEY `username` (`username`), + KEY `dept_id` (`dept_id`), + KEY `issuperuser` (`isadmin`), + KEY `group_id` (`group_id`,`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%syslog`; +CREATE TABLE `%TABLE_PREFIX%syslog` ( + `log_id` int(11) unsigned NOT NULL auto_increment, + `log_type` enum('Debug','Warning','Error') NOT NULL, + `title` varchar(255) NOT NULL, + `log` text NOT NULL, + `logger` varchar(64) NOT NULL, + `ip_address` varchar(16) NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`log_id`), + KEY `log_type` (`log_type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; +CREATE TABLE `%TABLE_PREFIX%ticket` ( + `ticket_id` int(11) unsigned NOT NULL auto_increment, + `ticketID` int(11) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '1', + `priority_id` int(10) unsigned NOT NULL default '2', + `topic_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `email` varchar(120) NOT NULL default '', + `name` varchar(32) NOT NULL default '', + `subject` varchar(64) NOT NULL default '[no subject]', + `helptopic` varchar(255) default NULL, + `phone` varchar(16) default NULL, + `phone_ext` varchar(8) default NULL, + `ip_address` varchar(16) NOT NULL default '', + `status` enum('open','closed') NOT NULL default 'open', + `source` enum('Web','Email','Phone','Other') NOT NULL default 'Other', + `isoverdue` tinyint(1) unsigned NOT NULL default '0', + `isanswered` tinyint(1) unsigned NOT NULL default '0', + `duedate` datetime default NULL, + `reopened` datetime default NULL, + `closed` datetime default NULL, + `lastmessage` datetime default NULL, + `lastresponse` datetime default NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`ticket_id`), + UNIQUE KEY `email_extid` (`ticketID`,`email`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `status` (`status`), + KEY `priority_id` (`priority_id`), + KEY `created` (`created`), + KEY `closed` (`closed`), + KEY `duedate` (`duedate`), + KEY `topic_id` (`topic_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`; +CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( + `attach_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `ref_id` int(11) unsigned NOT NULL default '0', + `ref_type` enum('M','R') NOT NULL default 'M', + `file_size` varchar(32) NOT NULL default '', + `file_name` varchar(128) NOT NULL default '', + `file_key` varchar(128) NOT NULL default '', + `deleted` tinyint(1) unsigned NOT NULL default '0', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime default NULL, + PRIMARY KEY (`attach_id`), + KEY `ticket_id` (`ticket_id`), + KEY `ref_type` (`ref_type`), + KEY `ref_id` (`ref_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`; +CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( + `lock_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `expire` datetime default NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`lock_id`), + UNIQUE KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_message`; +CREATE TABLE `%TABLE_PREFIX%ticket_message` ( + `msg_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `messageId` varchar(255) default NULL, + `message` text NOT NULL, + `headers` text, + `source` varchar(16) default NULL, + `ip_address` varchar(16) default NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime default NULL, + PRIMARY KEY (`msg_id`), + KEY `ticket_id` (`ticket_id`), + KEY `msgId` (`messageId`), + FULLTEXT KEY `message` (`message`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_note`; +CREATE TABLE `%TABLE_PREFIX%ticket_note` ( + `note_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `source` varchar(32) NOT NULL default '', + `title` varchar(255) NOT NULL default 'Generic Intermal Notes', + `note` text NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`note_id`), + KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`), + FULLTEXT KEY `note` (`note`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_priority`; +CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( + `priority_id` tinyint(4) NOT NULL auto_increment, + `priority` varchar(60) NOT NULL default '', + `priority_desc` varchar(30) NOT NULL default '', + `priority_color` varchar(7) NOT NULL default '', + `priority_urgency` tinyint(1) unsigned NOT NULL default '0', + `ispublic` tinyint(1) NOT NULL default '1', + PRIMARY KEY (`priority_id`), + UNIQUE KEY `priority` (`priority`), + KEY `priority_urgency` (`priority_urgency`), + KEY `ispublic` (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%ticket_priority` (`priority_id`, `priority`, `priority_desc`, `priority_color`, `priority_urgency`, `ispublic`) VALUES +(1, 'low', 'Low', '#DDFFDD', 4, 1), +(2, 'normal', 'Normal', '#FFFFF0', 3, 1), +(3, 'high', 'High', '#FEE7E7', 2, 1), +(4, 'emergency', 'Emergency', '#FEE7E7', 1, 0); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_response`; +CREATE TABLE `%TABLE_PREFIX%ticket_response` ( + `response_id` int(11) unsigned NOT NULL auto_increment, + `msg_id` int(11) unsigned NOT NULL default '0', + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL default '0', + `staff_name` varchar(32) NOT NULL default '', + `response` text NOT NULL, + `ip_address` varchar(16) NOT NULL default '', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`response_id`), + KEY `ticket_id` (`ticket_id`), + KEY `msg_id` (`msg_id`), + KEY `staff_id` (`staff_id`), + FULLTEXT KEY `response` (`response`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%timezone`; +CREATE TABLE `%TABLE_PREFIX%timezone` ( + `id` int(11) unsigned NOT NULL auto_increment, + `offset` float(3,1) NOT NULL default '0.0', + `timezone` varchar(255) NOT NULL default '', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%timezone` (`id`, `offset`, `timezone`) VALUES +(1, -12.0, 'Eniwetok, Kwajalein'), +(2, -11.0, 'Midway Island, Samoa'), +(3, -10.0, 'Hawaii'), +(4, -9.0, 'Alaska'), +(5, -8.0, 'Pacific Time (US & Canada)'), +(6, -7.0, 'Mountain Time (US & Canada)'), +(7, -6.0, 'Central Time (US & Canada), Mexico City'), +(8, -5.0, 'Eastern Time (US & Canada), Bogota, Lima'), +(9, -4.0, 'Atlantic Time (Canada), Caracas, La Paz'), +(10, -3.5, 'Newfoundland'), +(11, -3.0, 'Brazil, Buenos Aires, Georgetown'), +(12, -2.0, 'Mid-Atlantic'), +(13, -1.0, 'Azores, Cape Verde Islands'), +(14, 0.0, 'Western Europe Time, London, Lisbon, Casablanca'), +(15, 1.0, 'Brussels, Copenhagen, Madrid, Paris'), +(16, 2.0, 'Kaliningrad, South Africa'), +(17, 3.0, 'Baghdad, Riyadh, Moscow, St. Petersburg'), +(18, 3.5, 'Tehran'), +(19, 4.0, 'Abu Dhabi, Muscat, Baku, Tbilisi'), +(20, 4.5, 'Kabul'), +(21, 5.0, 'Ekaterinburg, Islamabad, Karachi, Tashkent'), +(22, 5.5, 'Bombay, Calcutta, Madras, New Delhi'), +(23, 6.0, 'Almaty, Dhaka, Colombo'), +(24, 7.0, 'Bangkok, Hanoi, Jakarta'), +(25, 8.0, 'Beijing, Perth, Singapore, Hong Kong'), +(26, 9.0, 'Tokyo, Seoul, Osaka, Sapporo, Yakutsk'), +(27, 9.5, 'Adelaide, Darwin'), +(28, 10.0, 'Eastern Australia, Guam, Vladivostok'), +(29, 11.0, 'Magadan, Solomon Islands, New Caledonia'), +(30, 12.0, 'Auckland, Wellington, Fiji, Kamchatka'); diff --git a/setup/inc/sql/osticket-v1.7-mysql.sql b/setup/inc/sql/osticket-v1.7-mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..a9966d536dd99e68378bc163e02da729b066352c --- /dev/null +++ b/setup/inc/sql/osticket-v1.7-mysql.sql @@ -0,0 +1,718 @@ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%api_key`; +CREATE TABLE `%TABLE_PREFIX%api_key` ( + `id` int(10) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) NOT NULL default '1', + `ipaddr` varchar(16) NOT NULL, + `apikey` varchar(255) NOT NULL, + `notes` text, + `updated` datetime NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `ipaddr` (`ipaddr`), + UNIQUE KEY `apikey` (`apikey`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq` ( + `faq_id` int(10) unsigned NOT NULL auto_increment, + `category_id` int(10) unsigned NOT NULL default '0', + `ispublished` tinyint(1) unsigned NOT NULL default '0', + `question` varchar(255) NOT NULL, + `answer` text NOT NULL, + `keywords` tinytext, + `notes` text, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`faq_id`), + UNIQUE KEY `question` (`question`), + KEY `category_id` (`category_id`), + KEY `ispublished` (`ispublished`), + FULLTEXT KEY `faq` (`question`,`answer`,`keywords`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_attachment` ( + `faq_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_category`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_category` ( + `category_id` int(10) unsigned NOT NULL auto_increment, + `ispublic` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0', + `name` varchar(125) default NULL, + `description` TEXT NOT NULL, + `notes` tinytext NOT NULL, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`category_id`), + KEY (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_topic`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_topic` ( + `faq_id` int(10) unsigned NOT NULL, + `topic_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`topic_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%config`; +CREATE TABLE `%TABLE_PREFIX%config` ( + `id` tinyint(1) unsigned NOT NULL auto_increment, + `isonline` tinyint(1) unsigned NOT NULL default '0', + `timezone_offset` float(3,1) NOT NULL default '0.0', + `enable_daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `staff_ip_binding` tinyint(1) unsigned NOT NULL default '1', + `staff_max_logins` tinyint(3) unsigned NOT NULL default '4', + `staff_login_timeout` int(10) unsigned NOT NULL default '2', + `staff_session_timeout` int(10) unsigned NOT NULL default '30', + `passwd_reset_period` int(10) unsigned NOT NULL default '0', + `client_max_logins` tinyint(3) unsigned NOT NULL default '4', + `client_login_timeout` int(10) unsigned NOT NULL default '2', + `client_session_timeout` int(10) unsigned NOT NULL default '30', + `max_page_size` tinyint(3) unsigned NOT NULL default '25', + `max_open_tickets` tinyint(3) unsigned NOT NULL default '0', + `max_file_size` int(11) unsigned NOT NULL default '1048576', + `max_user_file_uploads` tinyint(3) unsigned NOT NULL, + `max_staff_file_uploads` tinyint(3) unsigned NOT NULL, + `autolock_minutes` tinyint(3) unsigned NOT NULL default '3', + `overdue_grace_period` int(10) unsigned NOT NULL default '0', + `alert_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_dept_id` tinyint(3) unsigned NOT NULL default '0', + `default_sla_id` int(10) unsigned NOT NULL default '0', + `default_priority_id` tinyint(2) unsigned NOT NULL default '2', + `default_template_id` tinyint(4) unsigned NOT NULL default '1', + `default_timezone_id` int(10) unsigned NOT NULL default '0', + `default_smtp_id` tinyint(4) unsigned NOT NULL default '0', + `allow_email_spoofing` tinyint(1) unsigned NOT NULL default '0', + `clickable_urls` tinyint(1) unsigned NOT NULL default '1', + `allow_priority_change` tinyint(1) unsigned NOT NULL default '0', + `use_email_priority` tinyint(1) unsigned NOT NULL default '0', + `enable_kb` tinyint(1) unsigned NOT NULL default '1', + `enable_premade` tinyint(1) unsigned NOT NULL default '1', + `enable_captcha` tinyint(1) unsigned NOT NULL default '0', + `enable_auto_cron` tinyint(1) unsigned NOT NULL default '0', + `enable_mail_polling` tinyint(1) unsigned NOT NULL default '0', + `enable_email_piping` tinyint(1) unsigned NOT NULL default '0', + `send_sys_errors` tinyint(1) unsigned NOT NULL default '1', + `send_sql_errors` tinyint(1) unsigned NOT NULL default '1', + `send_mailparse_errors` tinyint(1) unsigned NOT NULL default '1', + `send_login_errors` tinyint(1) unsigned NOT NULL default '1', + `save_email_headers` tinyint(1) unsigned NOT NULL default '1', + `strip_quoted_reply` tinyint(1) unsigned NOT NULL default '1', + `log_ticket_activity` tinyint(1) unsigned NOT NULL default '1', + `ticket_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `message_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `ticket_notice_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_admin` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `message_alert_active` tinyint(1) unsigned NOT NULL default '0', + `message_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `message_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `message_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `note_alert_active` tinyint(1) unsigned NOT NULL default '0', + `note_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `note_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `note_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_active` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_assigned` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `transfer_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_active` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `assigned_alert_active` tinyint(1) unsigned NOT NULL default '1', + `assigned_alert_staff` tinyint(1) unsigned NOT NULL default '1', + `assigned_alert_team_lead` tinyint(1) unsigned NOT NULL default '0', + `assigned_alert_team_members` tinyint(1) unsigned NOT NULL default '0', + `auto_assign_reopened_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_related_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0', + `show_answered_tickets` tinyint(1) NOT NULL default '0', + `hide_staff_name` tinyint(1) unsigned NOT NULL default '0', + `overlimit_notice_active` tinyint(1) unsigned NOT NULL default '0', + `email_attachments` tinyint(1) unsigned NOT NULL default '1', + `allow_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_email_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments_onlogin` tinyint(1) unsigned NOT NULL default + '0', + `random_ticket_ids` tinyint(1) unsigned NOT NULL default '1', + `log_level` tinyint(1) unsigned NOT NULL default '2', + `log_graceperiod` int(10) unsigned NOT NULL default '12', + `upload_dir` varchar(255) NOT NULL default '', + `allowed_filetypes` varchar(255) NOT NULL default '.doc, .pdf', + `time_format` varchar(32) NOT NULL default ' h:i A', + `date_format` varchar(32) NOT NULL default 'm/d/Y', + `datetime_format` varchar(60) NOT NULL default 'm/d/Y g:i a', + `daydatetime_format` varchar(60) NOT NULL default 'D, M j Y g:ia', + `reply_separator` varchar(60) NOT NULL default '-- do not edit --', + `admin_email` varchar(125) NOT NULL default '', + `helpdesk_title` varchar(255) NOT NULL default + 'osTicket Support Ticket System', + `helpdesk_url` varchar(255) NOT NULL default '', + `schema_signature` char(32) NOT NULL default '', + `updated` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `isoffline` (`isonline`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; +CREATE TABLE `%TABLE_PREFIX%department` ( + `dept_id` int(11) unsigned NOT NULL auto_increment, + `tpl_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `autoresp_email_id` int(10) unsigned NOT NULL default '0', + `manager_id` int(10) unsigned NOT NULL default '0', + `dept_name` varchar(32) NOT NULL default '', + `dept_signature` tinytext NOT NULL, + `ispublic` tinyint(1) unsigned NOT NULL default '1', + `ticket_auto_response` tinyint(1) NOT NULL default '1', + `message_auto_response` tinyint(1) NOT NULL default '0', + `updated` datetime NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`dept_id`), + UNIQUE KEY `dept_name` (`dept_name`), + KEY `manager_id` (`manager_id`), + KEY `autoresp_email_id` (`autoresp_email_id`), + KEY `tpl_id` (`tpl_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%department` (`dept_id`, `tpl_id`, `sla_id`, `email_id`, `autoresp_email_id`, `manager_id`, `dept_name`, `dept_signature`, `ispublic`, `ticket_auto_response`, `message_auto_response`) VALUES + (1, 0, 0, 1, 1, 0, 'Support', 'Support Dept', 1, 1, 1), + (2, 0, 1, 1, 1, 0, 'Billing', 'Billing Dept', 1, 1, 1); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email`; +CREATE TABLE `%TABLE_PREFIX%email` ( + `email_id` int(11) unsigned NOT NULL auto_increment, + `noautoresp` tinyint(1) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '2', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `email` varchar(125) NOT NULL default '', + `name` varchar(32) NOT NULL default '', + `userid` varchar(125) NOT NULL, + `userpass` varchar(125) NOT NULL, + `mail_active` tinyint(1) NOT NULL default '0', + `mail_host` varchar(125) NOT NULL, + `mail_protocol` enum('POP','IMAP') NOT NULL default 'POP', + `mail_encryption` enum('NONE','SSL') NOT NULL, + `mail_port` int(6) default NULL, + `mail_fetchfreq` tinyint(3) NOT NULL default '5', + `mail_fetchmax` tinyint(4) NOT NULL default '30', + `mail_archivefolder` varchar(255) default NULL, + `mail_delete` tinyint(1) NOT NULL default '0', + `mail_errors` tinyint(3) NOT NULL default '0', + `mail_lasterror` datetime default NULL, + `mail_lastfetch` datetime default NULL, + `smtp_active` tinyint(1) default '0', + `smtp_host` varchar(125) NOT NULL, + `smtp_port` int(6) default NULL, + `smtp_secure` tinyint(1) NOT NULL default '1', + `smtp_auth` tinyint(1) NOT NULL default '1', + `smtp_spoofing` tinyint(1) unsigned NOT NULL default '0', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`email_id`), + UNIQUE KEY `email` (`email`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_filter`; +CREATE TABLE `%TABLE_PREFIX%email_filter` ( + `id` int(11) unsigned NOT NULL auto_increment, + `execorder` int(10) unsigned NOT NULL default '99', + `isactive` tinyint(1) unsigned NOT NULL default '1', + `match_all_rules` tinyint(1) unsigned NOT NULL default '0', + `stop_onmatch` tinyint(1) unsigned NOT NULL default '0', + `reject_email` tinyint(1) unsigned NOT NULL default '0', + `use_replyto_email` tinyint(1) unsigned NOT NULL default '0', + `disable_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `priority_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `name` varchar(32) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `email_id` (`email_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +INSERT INTO `%TABLE_PREFIX%email_filter` ( + `id`,`isactive`,`execorder`,`reject_email`,`name`,`notes`,`created`) + VALUES (1, 1, 99, 1, 'SYSTEM BAN LIST', 'Internal list for email banning. Do not remove', NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_filter_rule`; +CREATE TABLE `%TABLE_PREFIX%email_filter_rule` ( + `id` int(11) unsigned NOT NULL auto_increment, + `filter_id` int(10) unsigned NOT NULL default '0', + `what` enum('name','email','subject','body','header') NOT NULL, + `how` enum('equal','not_equal','contains','dn_contain') NOT NULL, + `val` varchar(255) NOT NULL, + `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1', + `notes` tinytext NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `filter_id` (`filter_id`), + UNIQUE `filter` (`filter_id`, `what`, `how`, `val`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%email_filter_rule` ( + `id`, `filter_id`, `isactive`, `what`,`how`,`val`,`created`) + VALUES (1, 1, 1, 'email', 'equal', 'test@example.com',NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_template`; +CREATE TABLE `%TABLE_PREFIX%email_template` ( + `tpl_id` int(11) NOT NULL auto_increment, + `cfg_id` int(10) unsigned NOT NULL default '0', + `isactive` tinyint(1) unsigned NOT NULL default '0', + `name` varchar(32) NOT NULL default '', + `notes` text, + `ticket_autoresp_subj` varchar(255) NOT NULL default '', + `ticket_autoresp_body` text NOT NULL, + `ticket_notice_subj` varchar(255) NOT NULL, + `ticket_notice_body` text NOT NULL, + `ticket_alert_subj` varchar(255) NOT NULL default '', + `ticket_alert_body` text NOT NULL, + `message_autoresp_subj` varchar(255) NOT NULL default '', + `message_autoresp_body` text NOT NULL, + `message_alert_subj` varchar(255) NOT NULL default '', + `message_alert_body` text NOT NULL, + `note_alert_subj` varchar(255) NOT NULL, + `note_alert_body` text NOT NULL, + `assigned_alert_subj` varchar(255) NOT NULL default '', + `assigned_alert_body` text NOT NULL, + `transfer_alert_subj` varchar(255) NOT NULL default '', + `transfer_alert_body` text NOT NULL, + `ticket_overdue_subj` varchar(255) NOT NULL default '', + `ticket_overdue_body` text NOT NULL, + `ticket_overlimit_subj` varchar(255) NOT NULL default '', + `ticket_overlimit_body` text NOT NULL, + `ticket_reply_subj` varchar(255) NOT NULL default '', + `ticket_reply_body` text NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`tpl_id`), + KEY `cfg_id` (`cfg_id`), + FULLTEXT KEY `message_subj` (`ticket_reply_subj`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%email_template` (`tpl_id`, `cfg_id`, `isactive`, `name`, `notes`, `ticket_autoresp_subj`, `ticket_autoresp_body`, `ticket_notice_subj`, `ticket_notice_body`, `ticket_alert_subj`, `ticket_alert_body`, `message_autoresp_subj`, `message_autoresp_body`, `message_alert_subj`, `message_alert_body`, `note_alert_subj`, `note_alert_body`, `assigned_alert_subj`, `assigned_alert_body`, `transfer_alert_subj`, `transfer_alert_body`, `ticket_overdue_subj`, `ticket_overdue_body`, `ticket_overlimit_subj`, `ticket_overlimit_body`, `ticket_reply_subj`, `ticket_reply_body`, `created`, `updated`) VALUES +(1, 1, 1, 'osTicket Default Template', 'Default osTicket templates', 'Support Ticket Opened [#%ticket]', '%name,\r\n\r\nA request for support has been created and assigned ticket #%ticket. A representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\nIf you wish to send additional comments or information regarding this issue, please don''t open a new ticket. Simply login using the link above and update the ticket.\r\n\r\n%signature', '[#%ticket] %subject', '%name,\r\n\r\nOur customer care team has created a ticket, #%ticket on your behalf, with the following message.\r\n\r\n%message\r\n\r\nIf you wish to provide additional comments or information regarding this issue, please don''t open a new ticket. You can update or view this ticket''s progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\n%signature', 'New Ticket Alert', '%staff,\r\n\r\nNew ticket #%ticket created.\r\n-------------------\r\nName: %name\r\nEmail: %email\r\nDept: %dept\r\n\r\n%message\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', '[#%ticket] Message Added', '%name,\r\n\r\nYour reply to support request #%ticket has been noted.\r\n\r\nYou can view this support request progress online here: %url/view.php?e=%email&t=%ticket.\r\n\r\n%signature', 'New Message Alert', '%staff,\r\n\r\nNew message appended to ticket #%ticket\r\n\r\n----------------------\r\nName: %name\r\nEmail: %email\r\nDept: %dept\r\n\r\n%message\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'New Internal Note Alert', '%staff,\r\n\r\nInternal note appended to ticket #%ticket\r\n\r\n----------------------\r\nName: %name\r\n\r\n%note\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Ticket #%ticket Assigned to you', '%assignee,\r\n\r\n%assigner has assigned ticket #%ticket to you or one of your teams!\r\n\r\n%note\r\n\r\nTo view complete details, simply login to the support system.\r\n\r\n%url/scp/tickets.php?id=%id\r\n\r\n- Your friendly Support Ticket System - powered by osTicket.', 'Ticket Transfer #%ticket - %dept', '%staff,\r\n\r\nTicket #%ticket has been transferred to %dept department\r\n\r\n----------------------\r\n\r\n%note\r\n\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%url/scp/ticket.php?id=%id\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Stale Ticket Alert', '%staff,\r\n\r\nA ticket, #%ticket assigned to you or in your department is seriously overdue.\r\n\r\n%url/scp/tickets.php?id=%id\r\n\r\nWe should all work hard to guarantee that all tickets are being addressed in a timely manner. Enough baby talk...please address the issue or you will hear from me again.\r\n\r\n\r\n- Your friendly (although with limited patience) Support Ticket System - powered by osTicket.', 'Support Ticket Denied', '%name\r\n\r\nNo support ticket has been created. You''ve exceeded maximum number of open tickets allowed.\r\n\r\nThis is a temporary block. To be able to open another ticket, one of your pending tickets must be closed. To update or add comments to an open ticket simply login using the link below.\r\n\r\n%url/view.php?e=%email\r\n\r\nThank you.\r\n\r\nSupport Ticket System', '[#%ticket] %subject', '%name,\r\n\r\nA customer support staff member has replied to your support request, #%ticket with the following response:\r\n\r\n%response\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not send another email. Instead, reply to this email or login to your account for a complete archive of all your support requests and responses.\r\n\r\n%url/view.php?e=%email&t=%ticket\r\n\r\n%signature', '2011-08-05 17:00:03', '2012-03-19 01:44:54'); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%file`; +CREATE TABLE `%TABLE_PREFIX%file` ( + `id` int(11) NOT NULL auto_increment, + `type` varchar(255) NOT NULL default '', + `size` varchar(25) NOT NULL default '', + `hash` varchar(125) NOT NULL, + `name` varchar(255) NOT NULL default '', + `filedata` longblob NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `hash` (`hash`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file` (`id`, `type`, `size`, `hash`, `name`, `filedata`, `created`) VALUES +(1, 'text/plain', '25', '670c6cc1d1dfc97fad20e5470251b255', 'osTicket.txt', 0x43616e6e6564206174746163686d656e747320726f636b210a, NOW()); + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`; +CREATE TABLE `%TABLE_PREFIX%groups` ( + `group_id` int(10) unsigned NOT NULL auto_increment, + `group_enabled` tinyint(1) unsigned NOT NULL default '1', + `group_name` varchar(50) NOT NULL default '', + `dept_access` varchar(255) NOT NULL default '', + `can_create_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_edit_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_delete_tickets` tinyint(1) unsigned NOT NULL default '0', + `can_close_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_assign_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_transfer_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_ban_emails` tinyint(1) unsigned NOT NULL default '0', + `can_manage_premade` tinyint(1) unsigned NOT NULL default '0', + `can_manage_faq` tinyint(1) unsigned NOT NULL default '0', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`group_id`), + KEY `group_active` (`group_enabled`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%groups` (`group_id`, `group_enabled`, `group_name`, `dept_access`, `can_create_tickets`, `can_edit_tickets`, `can_delete_tickets`, `can_close_tickets`, `can_assign_tickets`, `can_transfer_tickets`, `can_ban_emails`, `can_manage_premade`, `can_manage_faq`, `notes`) VALUES + (1, 1, 'Admins', '2,1', 1, 1, 1, 1, 1, 1, 1, 1, 1, 'overlords'), + (2, 1, 'Managers', '2,1', 1, 1, 1, 1, 1, 1, 1, 1, 1, ''), + (3, 1, 'Staff', '2,1', 1, 1, 0, 1, 1, 1, 0, 0, 0, ''); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic`; +CREATE TABLE `%TABLE_PREFIX%help_topic` ( + `topic_id` int(11) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) unsigned NOT NULL default '1', + `ispublic` tinyint(1) unsigned NOT NULL default '1', + `noautoresp` tinyint(3) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '0', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `topic` varchar(32) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`topic_id`), + UNIQUE KEY `topic` (`topic`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`,`team_id`), + KEY `sla_id` (`sla_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%help_topic` (`topic_id`, `isactive`, `ispublic`, `noautoresp`, `priority_id`, `dept_id`, `staff_id`, `team_id`, `sla_id`, `topic`, `notes`) VALUES + (1, 1, 1, 0, 2, 1, 0, 0, 1, 'Support', NULL), + (2, 1, 1, 0, 3, 1, 0, 0, 0, 'Billing', NULL); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_response`; +CREATE TABLE `%TABLE_PREFIX%canned_response` ( + `canned_id` int(10) unsigned NOT NULL auto_increment, + `dept_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `title` varchar(255) NOT NULL default '', + `response` text NOT NULL, + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`canned_id`), + UNIQUE KEY `title` (`title`), + KEY `dept_id` (`dept_id`), + KEY `active` (`isenabled`), + FULLTEXT KEY `resp` (`title`,`response`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%canned_response` (`canned_id`, `dept_id`, `isenabled`, `title`, `response`) VALUES + (1, 0, 1, 'What is osTicket (sample)?', '\r\nosTicket is a widely-used open source support ticket system, an attractive alternative to higher-cost and complex customer support systems - simple, lightweight, reliable, open source, web-based and easy to setup and use.'), + (2, 0, 1, 'Sample (with variables)', '\r\n%name,\r\n\r\nYour ticket #%ticket created on %createdate is in %dept department.\r\n\r\n'); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%canned_attachment` ( + `canned_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`canned_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%canned_attachment` (`canned_id`, `file_id`) VALUES (1,1); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; +CREATE TABLE `%TABLE_PREFIX%session` ( + `session_id` varchar(32) collate utf8_unicode_ci NOT NULL default '', + `session_data` longtext collate utf8_unicode_ci, + `session_expire` datetime default NULL, + `session_updated` datetime default NULL, + `user_id` int(10) unsigned NOT NULL default '0' COMMENT 'osTicket staff +ID', + `user_ip` varchar(32) collate utf8_unicode_ci NOT NULL, + `user_agent` varchar(255) collate utf8_unicode_ci NOT NULL, + PRIMARY KEY (`session_id`), + KEY `updated` (`session_updated`), + KEY `user_id` (`user_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%sla`; +CREATE TABLE `%TABLE_PREFIX%sla` ( + `id` int(11) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) unsigned NOT NULL default '1', + `enable_priority_escalation` tinyint(1) unsigned NOT NULL default '1', + `disable_overdue_alerts` tinyint(1) unsigned NOT NULL default '0', + `grace_period` int(10) unsigned NOT NULL default '0', + `name` varchar(64) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%sla` (`isactive`, `enable_priority_escalation`, + `disable_overdue_alerts`, `grace_period`, `name`, `notes`) + VALUES (1, 1, 0, 48, 'Default SLA', NULL); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%staff`; +CREATE TABLE `%TABLE_PREFIX%staff` ( + `staff_id` int(11) unsigned NOT NULL auto_increment, + `group_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `timezone_id` int(10) unsigned NOT NULL default '0', + `username` varchar(32) NOT NULL default '', + `firstname` varchar(32) default NULL, + `lastname` varchar(32) default NULL, + `passwd` varchar(128) default NULL, + `email` varchar(128) default NULL, + `phone` varchar(24) NOT NULL default '', + `phone_ext` varchar(6) default NULL, + `mobile` varchar(24) NOT NULL default '', + `signature` tinytext NOT NULL, + `notes` text, + `isactive` tinyint(1) NOT NULL default '1', + `isadmin` tinyint(1) NOT NULL default '0', + `isvisible` tinyint(1) unsigned NOT NULL default '1', + `onvacation` tinyint(1) unsigned NOT NULL default '0', + `assigned_only` tinyint(1) unsigned NOT NULL default '0', + `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0', + `daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `change_passwd` tinyint(1) unsigned NOT NULL default '0', + `max_page_size` int(11) unsigned NOT NULL default '0', + `auto_refresh_rate` int(10) unsigned NOT NULL default '0', + `default_signature_type` ENUM( 'none', 'mine', 'dept' ) NOT NULL DEFAULT 'none', + `created` datetime NOT NULL, + `lastlogin` datetime default NULL, + `passwdreset` datetime default NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`staff_id`), + UNIQUE KEY `username` (`username`), + KEY `dept_id` (`dept_id`), + KEY `issuperuser` (`isadmin`), + KEY `group_id` (`group_id`,`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%syslog`; +CREATE TABLE `%TABLE_PREFIX%syslog` ( + `log_id` int(11) unsigned NOT NULL auto_increment, + `log_type` enum('Debug','Warning','Error') NOT NULL, + `title` varchar(255) NOT NULL, + `log` text NOT NULL, + `logger` varchar(64) NOT NULL, + `ip_address` varchar(16) NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`log_id`), + KEY `log_type` (`log_type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%team`; +CREATE TABLE `%TABLE_PREFIX%team` ( + `team_id` int(10) unsigned NOT NULL auto_increment, + `lead_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `noalerts` tinyint(1) unsigned NOT NULL default '0', + `name` varchar(125) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`team_id`), + UNIQUE KEY `name` (`name`), + KEY `isnabled` (`isenabled`), + KEY `lead_id` (`lead_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%team` (`lead_id`, `isenabled`, `noalerts`, `name`, `notes`) + VALUES (0, 1, 0, 'Level I Support', ''); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%team_member`; +CREATE TABLE `%TABLE_PREFIX%team_member` ( + `team_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`team_id`,`staff_id`) +) ENGINE=MyISAM; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; +CREATE TABLE `%TABLE_PREFIX%ticket` ( + `ticket_id` int(11) unsigned NOT NULL auto_increment, + `ticketID` int(11) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '1', + `sla_id` int(10) unsigned NOT NULL default '0', + `priority_id` int(10) unsigned NOT NULL default '2', + `topic_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `email` varchar(120) NOT NULL default '', + `name` varchar(32) NOT NULL default '', + `subject` varchar(64) NOT NULL default '[no subject]', + `phone` varchar(16) default NULL, + `phone_ext` varchar(8) default NULL, + `ip_address` varchar(16) NOT NULL default '', + `status` enum('open','closed') NOT NULL default 'open', + `source` enum('Web','Email','Phone','API','Other') NOT NULL default +'Other', + `isoverdue` tinyint(1) unsigned NOT NULL default '0', + `isanswered` tinyint(1) unsigned NOT NULL default '0', + `duedate` datetime default NULL, + `reopened` datetime default NULL, + `closed` datetime default NULL, + `lastmessage` datetime default NULL, + `lastresponse` datetime default NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`ticket_id`), + UNIQUE KEY `email_extid` (`ticketID`,`email`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`staff_id`), + KEY `status` (`status`), + KEY `priority_id` (`priority_id`), + KEY `created` (`created`), + KEY `closed` (`closed`), + KEY `duedate` (`duedate`), + KEY `topic_id` (`topic_id`), + KEY `sla_id` (`sla_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`; +CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( + `attach_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `file_id` int(10) unsigned NOT NULL default '0', + `ref_id` int(11) unsigned NOT NULL default '0', + `ref_type` enum('M','R','N') NOT NULL default 'M', + `created` datetime NOT NULL, + PRIMARY KEY (`attach_id`), + KEY `ticket_id` (`ticket_id`), + KEY `ref_type` (`ref_type`), + KEY `ref_id` (`ref_id`), + KEY `file_id` (`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`; +CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( + `lock_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `expire` datetime default NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`lock_id`), + UNIQUE KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_history`; +CREATE TABLE `%TABLE_PREFIX%ticket_history` ( + `ticket_id` int(11) unsigned NOT NULL default '0', + `state` enum('opened','closed','assigned','transferred','overdue') NOT NULL, + `staff` varchar(255) NOT NULL default 'SYSTEM', + `timestamp` datetime NOT NULL, + KEY `ticket_state` (`ticket_id`, `state`, `timestamp`), + KEY `ticket_stats` (`timestamp`, `state`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_message`; +CREATE TABLE `%TABLE_PREFIX%ticket_message` ( + `msg_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `messageId` varchar(255) default NULL, + `message` text NOT NULL, + `headers` text, + `source` varchar(16) default NULL, + `ip_address` varchar(16) default NULL, + `created` datetime NOT NULL, + `updated` datetime default NULL, + PRIMARY KEY (`msg_id`), + KEY `ticket_id` (`ticket_id`), + KEY `msgId` (`messageId`), + FULLTEXT KEY `message` (`message`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_note`; +CREATE TABLE `%TABLE_PREFIX%ticket_note` ( + `note_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `source` varchar(32) NOT NULL default '', + `title` varchar(255) NOT NULL default 'Generic Intermal Notes', + `note` text NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`note_id`), + KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`), + FULLTEXT KEY `note` (`note`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_priority`; +CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( + `priority_id` tinyint(4) NOT NULL auto_increment, + `priority` varchar(60) NOT NULL default '', + `priority_desc` varchar(30) NOT NULL default '', + `priority_color` varchar(7) NOT NULL default '', + `priority_urgency` tinyint(1) unsigned NOT NULL default '0', + `ispublic` tinyint(1) NOT NULL default '1', + PRIMARY KEY (`priority_id`), + UNIQUE KEY `priority` (`priority`), + KEY `priority_urgency` (`priority_urgency`), + KEY `ispublic` (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%ticket_priority` (`priority_id`, `priority`, `priority_desc`, `priority_color`, `priority_urgency`, `ispublic`) VALUES + (1, 'low', 'Low', '#DDFFDD', 4, 1), + (2, 'normal', 'Normal', '#FFFFF0', 3, 1), + (3, 'high', 'High', '#FEE7E7', 2, 1), + (4, 'emergency', 'Emergency', '#FEE7E7', 1, 0); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_response`; +CREATE TABLE `%TABLE_PREFIX%ticket_response` ( + `response_id` int(11) unsigned NOT NULL auto_increment, + `msg_id` int(11) unsigned NOT NULL default '0', + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL default '0', + `staff_name` varchar(32) NOT NULL default '', + `response` text NOT NULL, + `ip_address` varchar(16) NOT NULL default '', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`response_id`), + KEY `ticket_id` (`ticket_id`), + KEY `msg_id` (`msg_id`), + KEY `staff_id` (`staff_id`), + FULLTEXT KEY `response` (`response`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%timezone`; +CREATE TABLE `%TABLE_PREFIX%timezone` ( + `id` int(11) unsigned NOT NULL auto_increment, + `offset` float(3,1) NOT NULL default '0.0', + `timezone` varchar(255) NOT NULL default '', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%timezone` (`id`, `offset`, `timezone`) VALUES +(1, -12.0, 'Eniwetok, Kwajalein'), +(2, -11.0, 'Midway Island, Samoa'), +(3, -10.0, 'Hawaii'), +(4, -9.0, 'Alaska'), +(5, -8.0, 'Pacific Time (US & Canada)'), +(6, -7.0, 'Mountain Time (US & Canada)'), +(7, -6.0, 'Central Time (US & Canada), Mexico City'), +(8, -5.0, 'Eastern Time (US & Canada), Bogota, Lima'), +(9, -4.0, 'Atlantic Time (Canada), Caracas, La Paz'), +(10, -3.5, 'Newfoundland'), +(11, -3.0, 'Brazil, Buenos Aires, Georgetown'), +(12, -2.0, 'Mid-Atlantic'), +(13, -1.0, 'Azores, Cape Verde Islands'), +(14, 0.0, 'Western Europe Time, London, Lisbon, Casablanca'), +(15, 1.0, 'Brussels, Copenhagen, Madrid, Paris'), +(16, 2.0, 'Kaliningrad, South Africa'), +(17, 3.0, 'Baghdad, Riyadh, Moscow, St. Petersburg'), +(18, 3.5, 'Tehran'), +(19, 4.0, 'Abu Dhabi, Muscat, Baku, Tbilisi'), +(20, 4.5, 'Kabul'), +(21, 5.0, 'Ekaterinburg, Islamabad, Karachi, Tashkent'), +(22, 5.5, 'Bombay, Calcutta, Madras, New Delhi'), +(23, 6.0, 'Almaty, Dhaka, Colombo'), +(24, 7.0, 'Bangkok, Hanoi, Jakarta'), +(25, 8.0, 'Beijing, Perth, Singapore, Hong Kong'), +(26, 9.0, 'Tokyo, Seoul, Osaka, Sapporo, Yakutsk'), +(27, 9.5, 'Adelaide, Darwin'), +(28, 10.0, 'Eastern Australia, Guam, Vladivostok'), +(29, 11.0, 'Magadan, Solomon Islands, New Caledonia'), +(30, 12.0, 'Auckland, Wellington, Fiji, Kamchatka'); diff --git a/setup/inc/sql/v1.6rc5-upgrade.sql b/setup/inc/sql/v1.6rc5-upgrade.sql new file mode 100644 index 0000000000000000000000000000000000000000..9f44710fe3bb1c4523433619c59e31e01bd94f73 --- /dev/null +++ b/setup/inc/sql/v1.6rc5-upgrade.sql @@ -0,0 +1,128 @@ +ALTER TABLE `%TABLE_PREFIX%email` +ADD `userid` VARCHAR( 125 ) NOT NULL AFTER `name` , +ADD `userpass` VARCHAR( 125 ) NOT NULL AFTER `userid`, +ADD `mail_active` TINYINT( 1 ) NOT NULL DEFAULT '0' AFTER `userpass` , +ADD `mail_host` VARCHAR( 125 ) NOT NULL AFTER `mail_active` , +ADD `mail_protocol` ENUM( 'POP', 'IMAP' ) NOT NULL AFTER `mail_host` , +ADD `mail_encryption` ENUM( 'NONE', 'SSL' ) NOT NULL AFTER `mail_protocol` , +ADD `mail_port` INT( 6 ) NULL AFTER `mail_encryption` , +ADD `mail_fetchfreq` TINYINT( 3 ) NOT NULL DEFAULT '5' AFTER `mail_port` , +ADD `mail_fetchmax` TINYINT( 4 ) NOT NULL DEFAULT '30' AFTER `mail_fetchfreq` , +ADD `mail_delete` TINYINT( 1 ) NOT NULL DEFAULT '0' AFTER `mail_fetchmax` , +ADD `mail_errors` TINYINT( 3 ) NOT NULL DEFAULT '0' AFTER `mail_delete` , +ADD `mail_lasterror` DATETIME NULL AFTER `mail_errors` , +ADD `mail_lastfetch` DATETIME NULL AFTER `mail_lasterror` , +ADD `smtp_active` TINYINT( 1 ) NOT NULL AFTER `mail_lastfetch` , +ADD `smtp_host` VARCHAR( 125 ) NOT NULL AFTER `smtp_active` , +ADD `smtp_port` INT( 6 ) NULL AFTER `smtp_host` , +ADD `smtp_auth` TINYINT( 1 ) NOT NULL DEFAULT '1' AFTER `smtp_port` ; + +ALTER TABLE `%TABLE_PREFIX%groups` ADD `can_edit_tickets` TINYINT UNSIGNED NOT NULL DEFAULT '0' AFTER `dept_access` ; + +UPDATE `%TABLE_PREFIX%groups` SET `can_edit_tickets`=1 WHERE `can_delete_tickets`=1; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `duedate` DATETIME NULL AFTER `isoverdue` ; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD INDEX ( `duedate` ) ; + +ALTER TABLE `%TABLE_PREFIX%ticket` CHANGE `source` `source` ENUM( 'Web', 'Email', 'Phone', 'Other' ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'Other' ; + +ALTER TABLE `%TABLE_PREFIX%email_template` +ADD `note_alert_subj` VARCHAR( 255 ) NOT NULL AFTER `message_alert_body` , +ADD `note_alert_body` TEXT NOT NULL AFTER `note_alert_subj` ; + +UPDATE `%TABLE_PREFIX%email_template` SET `note_alert_subj` = 'New Internal Note Alert',`note_alert_body` = '%staff,\r\n\r\nInternal note appended to ticket #%ticket\r\n\r\n----------------------\r\nName: %name\r\n\r\n%note\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\nYour friendly,\r\n\r\nCustomer Support System - powered by osTicket.'; + +ALTER TABLE `%TABLE_PREFIX%ticket_message` ADD `messageId` VARCHAR( 255 ) NULL AFTER `ticket_id` ; +ALTER TABLE `%TABLE_PREFIX%ticket_message` ADD INDEX ( `messageId` ) ; + +ALTER TABLE `%TABLE_PREFIX%config` +ADD `note_alert_active` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `message_alert_dept_manager` , +ADD `note_alert_laststaff` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `note_alert_active` , +ADD `note_alert_assigned` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `note_alert_laststaff` , +ADD `note_alert_dept_manager` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `note_alert_assigned` ; + +ALTER TABLE `%TABLE_PREFIX%department` ADD `autoresp_email_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `email_id` ; + +ALTER TABLE `%TABLE_PREFIX%department` ADD INDEX ( `autoresp_email_id` ) ; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `default_priority` `default_priority_id` TINYINT( 2 ) UNSIGNED NOT NULL DEFAULT '2'; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `default_template` `default_template_id` TINYINT( 4 ) UNSIGNED NOT NULL DEFAULT '1'; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `default_email` `default_email_id` TINYINT( 4 ) UNSIGNED NOT NULL DEFAULT '0'; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `default_dept` `default_dept_id` TINYINT( 3 ) UNSIGNED NOT NULL DEFAULT '0'; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `enable_pop3_fetch` `enable_mail_fetch` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0'; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `alert_email_id` TINYINT( 4 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `overdue_grace_period` ; + +ALTER TABLE `%TABLE_PREFIX%config` +ADD `default_smtp_id` TINYINT( 4 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `default_template_id` , +ADD `spoof_default_smtp` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `default_smtp_id` ; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `log_level` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '2' AFTER `random_ticket_ids` ; + +ALTER TABLE `%TABLE_PREFIX%config` +ADD `staff_max_logins` TINYINT UNSIGNED NOT NULL DEFAULT '4' AFTER `enable_daylight_saving` , +ADD `staff_login_timeout` INT UNSIGNED NOT NULL DEFAULT '2' AFTER `staff_max_logins` ; + +ALTER TABLE `%TABLE_PREFIX%config` +ADD `client_max_logins` TINYINT UNSIGNED NOT NULL DEFAULT '4' AFTER `staff_session_timeout` , +ADD `client_login_timeout` INT UNSIGNED NOT NULL DEFAULT '2' AFTER `client_max_logins` ; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `show_answered_tickets` TINYINT( 1 ) NOT NULL DEFAULT '0' AFTER `show_assigned_tickets` ; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `isanswered` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `isoverdue` ; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `lastmessage` DATETIME NULL AFTER `closed` , ADD `lastresponse` DATETIME NULL AFTER `lastmessage` ; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `hide_staff_name` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `show_answered_tickets` ; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `log_graceperiod` INT UNSIGNED NOT NULL DEFAULT '12' AFTER `log_level` ; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `phone_ext` VARCHAR( 8 ) NULL DEFAULT NULL AFTER `phone` ; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `topic` VARCHAR(64) NULL DEFAULT NULL AFTER `subject` ; + +ALTER TABLE `%TABLE_PREFIX%ticket_message` ADD FULLTEXT (`message`); + +ALTER TABLE `%TABLE_PREFIX%ticket_response` ADD FULLTEXT (`response`); + +ALTER TABLE `%TABLE_PREFIX%ticket_note` ADD FULLTEXT (`note`); + +ALTER TABLE `%TABLE_PREFIX%department` ADD `tpl_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `dept_id` ; + +ALTER TABLE `%TABLE_PREFIX%department` ADD INDEX ( `tpl_id` ) ; + +ALTER TABLE `%TABLE_PREFIX%email_template` ADD `notes` TEXT NULL AFTER `name` ; + +ALTER TABLE `%TABLE_PREFIX%config` CHANGE `api_key` `api_passphrase` VARCHAR( 125 ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%api_key`; +CREATE TABLE `%TABLE_PREFIX%api_key` ( + `id` int(10) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) NOT NULL default '1', + `ipaddr` varchar(16) NOT NULL, + `apikey` varchar(255) NOT NULL, + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + UNIQUE KEY `ipaddr` (`ipaddr`) +) ENGINE=MyISAM; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%syslog`; +CREATE TABLE `%TABLE_PREFIX%syslog` ( + `log_id` int(11) unsigned NOT NULL auto_increment, + `log_type` enum('Debug','Warning','Error') NOT NULL, + `title` varchar(255) NOT NULL, + `log` text NOT NULL, + `logger` varchar(64) NOT NULL, + `ip_address` varchar(16) NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `updated` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`log_id`), + KEY `log_type` (`log_type`) +) ENGINE=MyISAM; + diff --git a/setup/inc/sql/v1.6st-1.7-upgrade-mysql.sql b/setup/inc/sql/v1.6st-1.7-upgrade-mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..eb7442a5f9f4a13191f7d0cd1c97f8c5c8ec3d4b --- /dev/null +++ b/setup/inc/sql/v1.6st-1.7-upgrade-mysql.sql @@ -0,0 +1,285 @@ +-- Add a table to contain the attachment file contents +DROP TABLE IF EXISTS `%TABLE_PREFIX%file`; +CREATE TABLE `%TABLE_PREFIX%file` ( + `id` int(11) NOT NULL auto_increment, + `type` varchar(255) NOT NULL default '', + `size` varchar(25) NOT NULL default '', + `hash` varchar(125) NOT NULL, + `name` varchar(255) NOT NULL default '', + `filedata` longblob NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `hash` (`hash`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- update ticket attachments ref. table. +ALTER TABLE `%TABLE_PREFIX%ticket_attachment` + CHANGE `ref_type` `ref_type` ENUM( 'M', 'R', 'N' ) NOT NULL DEFAULT 'M', + ADD `file_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `ticket_id`, + ADD INDEX ( `file_id` ); + +-- Add Team ID and 'API' as a valid ticket source +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `team_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `staff_id`, + ADD INDEX ( `team_id` ), + CHANGE `source` `source` ENUM( + 'Web', 'Email', 'Phone', 'API', 'Other') NOT NULL DEFAULT 'Other'; + +-- Add table for ticket history (statistics) tracking +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_history`; +CREATE TABLE `%TABLE_PREFIX%ticket_history` ( + `ticket_id` int(11) unsigned NOT NULL default '0', + `state` enum('opened','closed','assigned','transferred','overdue') NOT NULL, + `staff` varchar(255) NOT NULL default 'SYSTEM', + `timestamp` datetime NOT NULL, + KEY `ticket_state` (`ticket_id`, `state`, `timestamp`), + KEY `ticket_stats` (`timestamp`, `state`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +ALTER TABLE `%TABLE_PREFIX%config` + ADD `passwd_reset_period` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `staff_session_timeout`, + ADD `default_timezone_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `default_template_id`, + ADD `default_sla_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `default_dept_id`, + CHANGE `spoof_default_smtp` `allow_email_spoofing` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + CHANGE `enable_mail_fetch` `enable_mail_polling` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + ADD `max_user_file_uploads` TINYINT UNSIGNED NOT NULL AFTER `max_file_size`, + ADD `max_staff_file_uploads` TINYINT UNSIGNED NOT NULL AFTER `max_user_file_uploads`, + ADD `assigned_alert_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `overdue_alert_dept_members`, + ADD `assigned_alert_staff` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `assigned_alert_active`, + ADD `assigned_alert_team_lead` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `assigned_alert_staff`, + ADD `assigned_alert_team_members` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `assigned_alert_team_lead`, + ADD `transfer_alert_active` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `note_alert_dept_manager` , + ADD `transfer_alert_assigned` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `transfer_alert_active` , + ADD `transfer_alert_dept_manager` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `transfer_alert_assigned` , + ADD `transfer_alert_dept_members` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `transfer_alert_dept_manager`, + ADD `send_sys_errors` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `enable_email_piping`, + ADD `enable_kb` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `use_email_priority`, + ADD `enable_premade` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `enable_kb`, + ADD `show_related_tickets` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `auto_assign_reopened_tickets`, + ADD `schema_signature` CHAR( 32 ) NOT NULL AFTER `ostversion`; + +ALTER TABLE `%TABLE_PREFIX%staff` + ADD `passwdreset` DATETIME NULL DEFAULT NULL AFTER `lastlogin`; + +DROP TABLE IF EXISTS `%TICKET_PREFIX%sla`; +CREATE TABLE IF NOT EXISTS `%TICKET_PREFIX%sla` ( + `id` int(11) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) unsigned NOT NULL default '1', + `enable_priority_escalation` tinyint(1) unsigned NOT NULL default '1', + `disable_overdue_alerts` tinyint(1) unsigned NOT NULL default '0', + `grace_period` int(10) unsigned NOT NULL default '0', + `name` varchar(64) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=MyISAM; + +-- Create a default SLA +INSERT INTO `%TABLE_PREFIX%sla` (`isactive`, `enable_priority_escalation`, + `disable_overdue_alerts`, `grace_period`, `name`, `notes`) + VALUES (1, 1, 0, 48, 'Default SLA', NULL); + +-- Create a TEAM table +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%team` ( + `team_id` int(10) unsigned NOT NULL auto_increment, + `lead_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `noalerts` tinyint(1) unsigned NOT NULL default '0', + `name` varchar(125) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`team_id`), + UNIQUE KEY `name` (`name`), + KEY `isnabled` (`isenabled`), + KEY `lead_id` (`lead_id`) +) ENGINE=MyISAM; + +-- Create a default TEAM +INSERT INTO `%TABLE_PREFIX%team` (`lead_id`, `isenabled`, `noalerts`, `name`, `notes`) + VALUES (0, 1, 0, 'Level I Support', ''); + +ALTER TABLE `%TABLE_PREFIX%department` + ADD sla_id INT UNSIGNED NOT NULL DEFAULT '0' AFTER tpl_id; + +ALTER TABLE `%TABLE_PREFIX%staff` + ADD `notes` TEXT NULL DEFAULT NULL AFTER `signature`, + ADD `assigned_only` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `onvacation`, + ADD `show_assigned_tickets` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `assigned_only`, + ADD `timezone_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `dept_id`, + ADD `default_signature_type` ENUM( 'none', 'mine', 'dept' ) NOT NULL DEFAULT 'none' AFTER `auto_refresh_rate`; + +-- Copy over time zone offet to tz_id +UPDATE `%TABLE_PREFIX%staff` SET timezone_id = + (SELECT id FROM `%TABLE_PREFIX%timezone` WHERE offset = `%TABLE_PREFIX%staff`.timezone_offset); + +ALTER TABLE `%TABLE_PREFIX%groups` + ADD notes TEXT NULL AFTER can_manage_kb, + CHANGE `can_manage_kb` `can_manage_premade` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + ADD `can_manage_faq` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `can_manage_premade`, + ADD `can_assign_tickets` TINYINT( 1 ) UNSIGNED NOT NULL default '1' AFTER `can_close_tickets`; + +-- Add new columns to the templates table +ALTER TABLE `%TABLE_PREFIX%email_template` + ADD `isactive` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `cfg_id`, + ADD `transfer_alert_subj` VARCHAR( 255 ) NOT NULL AFTER `assigned_alert_body`, + ADD `transfer_alert_body` TEXT NOT NULL AFTER `transfer_alert_subj`; + +-- Insert default text for the new messaage tpl (all records are updated). +UPDATE `%TABLE_PREFIX%email_template` SET updated=NOW() ,transfer_alert_subj='Ticket Transfer #%ticket - %dept',transfer_alert_body='%staff,\r\n\r\nTicket #%ticket has been transferred to %dept department.\r\n\r\n----------------------\r\n\r\n%note\r\n\r\n-------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%url/scp/ticket.php?id=%id\r\n\r\n- Your friendly Customer Support System - powered by osTicket.'; + +ALTER TABLE `%TABLE_PREFIX%help_topic` + ADD ispublic TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER isactive, + ADD notes TEXT NULL DEFAULT NULL AFTER topic, + ADD staff_id INT UNSIGNED NOT NULL DEFAULT '0' AFTER dept_id, + ADD team_id INT UNSIGNED NOT NULL DEFAULT '0' AFTER staff_id, + ADD sla_id INT UNSIGNED NOT NULL DEFAULT '0' AFTER team_id, + ADD INDEX ( staff_id , team_id ), + ADD INDEX ( sla_id ); + +ALTER TABLE `%TABLE_PREFIX%email` + ADD mail_archivefolder VARCHAR(255) NULL AFTER mail_fetchmax, + ADD notes TEXT NULL DEFAULT NULL AFTER smtp_auth, + ADD smtp_spoofing TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER smtp_auth; + +ALTER TABLE `%TABLE_PREFIX%api_key` + ADD notes TEXT NULL DEFAULT NULL AFTER apikey, + ADD UNIQUE (apikey); + +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD sla_id INT UNSIGNED NOT NULL DEFAULT '0' AFTER dept_id, + ADD INDEX ( sla_id ); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_filter`; +CREATE TABLE `%TABLE_PREFIX%email_filter` ( + `id` int(11) unsigned NOT NULL auto_increment, + `execorder` int(10) unsigned NOT NULL default '99', + `isactive` tinyint(1) unsigned NOT NULL default '1', + `match_all_rules` tinyint(1) unsigned NOT NULL default '0', + `stop_onmatch` tinyint(1) unsigned NOT NULL default '0', + `reject_email` tinyint(1) unsigned NOT NULL default '0', + `use_replyto_email` tinyint(1) unsigned NOT NULL default '0', + `disable_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `priority_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `name` varchar(32) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `email_id` (`email_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- Copy banlist to a new email filter +INSERT INTO `%TABLE_PREFIX%email_filter` (`id`, `execorder`, `isactive`, + `match_all_rules`, `stop_onmatch`, `reject_email`, `use_replyto_email`, + `disable_autoresponder`, `email_id`, `priority_id`, `dept_id`, `staff_id`, + `team_id`, `sla_id`, `name`, `notes`) VALUES + (1, 99, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 'SYSTEM BAN LIST', + 'Internal list for email banning. Do not remove'); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_filter_rule`; +CREATE TABLE `%TABLE_PREFIX%email_filter_rule` ( + `id` int(11) unsigned NOT NULL auto_increment, + `filter_id` int(10) unsigned NOT NULL default '0', + `what` enum('name','email','subject','body','header') NOT NULL, + `how` enum('equal','not_equal','contains','dn_contain') NOT NULL, + `val` varchar(255) NOT NULL, + `isactive` tinytext( 1 ) UNSIGNED NOT NULL DEFAULT '1', + `notes` tinytext NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `filter_id` (`filter_id`), + UNIQUE `filter` (`filter_id`, `what`, `how`, `val`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- SYSTEM BAN LIST was the first filter created, with ID of '1' +INSERT INTO `%TABLE_PREFIX%email_filter_rule` (`filter_id`, `what`, `how`, `val`) VALUES +SELECT 1, 'email', 'equals', email FROM `%TABLE_PREFIX%email_banlist`; + +-- Create table session +DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; +CREATE TABLE `%TABLE_PREFIX%session` ( + `session_id` varchar(32) collate utf8_unicode_ci NOT NULL default '', + `session_data` longtext collate utf8_unicode_ci, + `session_expire` datetime default NULL, + `session_updated` datetime default NULL, + `user_id` int(10) unsigned NOT NULL default '0', + `user_ip` varchar(32) collate utf8_unicode_ci NOT NULL, + `user_agent` varchar(255) collate utf8_unicode_ci NOT NULL, + PRIMARY KEY (`session_id`), + KEY `updated` (`session_updated`), + KEY `user_id` (`user_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- Create tables for FAQ + attachments. +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq` ( + `faq_id` int(10) unsigned NOT NULL auto_increment, + `category_id` int(10) unsigned NOT NULL default '0', + `ispublished` tinyint(1) unsigned NOT NULL default '0', + `question` varchar(255) NOT NULL, + `answer` text NOT NULL, + `keywords` tinytext, + `notes` text, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`faq_id`), + UNIQUE KEY `question` (`question`), + KEY `category_id` (`category_id`), + KEY `ispublished` (`ispublished`), + FULLTEXT KEY `faq` (`question`,`answer`,`keywords`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_attachment` ( + `faq_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- Add support for attachments to canned responses +DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%canned_attachment` ( + `canned_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`canned_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- Rename kb_premade to canned_response +ALTER TABLE `%TABLE_PREFIX%kb_premade` + ADD `notes` TEXT NOT NULL AFTER `answer`, + CHANGE `premade_id` `canned_id` int(10) unsigned NOT NULL auto_increment, + CHANGE `title` `title` VARCHAR( 255 ) NOT NULL DEFAULT '', + CHANGE `answer` `response` TEXT NOT NULL. + DROP INDEX `title` , + ADD FULLTEXT `resp` (`title` ,`answer`); + +ALTER TABLE `%TABLE_PREFIX%kb_premade` RENAME TO `%TABLE_PREFIX%canned_response`; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_category`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_category` ( + `category_id` int(10) unsigned NOT NULL auto_increment, + `ispublic` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0', + `name` varchar(125) default NULL, + `description` TEXT NOT NULL, + `notes` tinytext NOT NULL, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`category_id`), + KEY (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_topic`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_topic` ( + `faq_id` int(10) unsigned NOT NULL, + `topic_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`topic_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/setup/inc/sql/v1.6st-upgrade.sql b/setup/inc/sql/v1.6st-upgrade.sql new file mode 100644 index 0000000000000000000000000000000000000000..571620fb1beddc9137ea77c133b7bc47cc7a6960 --- /dev/null +++ b/setup/inc/sql/v1.6st-upgrade.sql @@ -0,0 +1,28 @@ +ALTER TABLE `%TABLE_PREFIX%ticket` ADD `topic_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `priority_id`; + +ALTER TABLE `%TABLE_PREFIX%ticket` ADD INDEX ( `topic_id` ); + +ALTER TABLE `%TABLE_PREFIX%ticket` CHANGE `topic` `helptopic` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL; + +ALTER TABLE `%TABLE_PREFIX%groups` ADD `can_create_tickets` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `dept_access`; + +ALTER TABLE `%TABLE_PREFIX%staff` ADD `auto_refresh_rate` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `max_page_size`; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `ticket_notice_active` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `message_autoresponder`; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `enable_captcha` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `use_email_priority`; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `log_ticket_activity` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `strip_quoted_reply`; + +ALTER TABLE `%TABLE_PREFIX%config` ADD `staff_ip_binding` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `enable_daylight_saving`; + +ALTER TABLE `%TABLE_PREFIX%staff` CHANGE `signature` `signature` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL; + +ALTER TABLE `%TABLE_PREFIX%department` CHANGE `dept_signature` `dept_signature` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL; + +ALTER TABLE `%TABLE_PREFIX%email_template` +ADD `ticket_notice_subj` VARCHAR( 255 ) NOT NULL AFTER `ticket_autoresp_body` , +ADD `ticket_notice_body` TEXT NOT NULL AFTER `ticket_notice_subj`; + +INSERT INTO `%TABLE_PREFIX%kb_premade` (`premade_id`, `dept_id`, `isenabled`, `title`, `answer`, `created`, `updated`) VALUES + ('', 0, 1, 'Sample (with variables)', '\r\n%name,\r\n\r\nYour ticket #%ticket created on %createdate is in %dept department.\r\n\r\n', NOW(), NOW()); diff --git a/setup/inc/sql/v1.7-cleanup-mysql.sql b/setup/inc/sql/v1.7-cleanup-mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..6464a100101f330cc8b3425adbd27104721ac632 --- /dev/null +++ b/setup/inc/sql/v1.7-cleanup-mysql.sql @@ -0,0 +1,23 @@ +-- Drop fields we no longer need in the reference table. +ALTER TABLE `%TABLE_PREFIX%ticket_attachment` + DROP `file_size`, + DROP `file_name`, + DROP `file_key`, + DROP `updated`, + DROP `isdeleted`; + +-- Drop fields we no longer need in config table. +ALTER TABLE `%TABLE_PREFIX%config` + DROP `api_passphrase`; + +-- Drop fields we no longer need in staff table. +ALTER TABLE `%TABLE_PREFIX%staff` + DROP `append_signature`, + DROP `timezone_offset`; + +-- Drop fields we no longer need in department table. +ALTER TABLE `%TABLE_PREFIX%department` + DROP `can_append_signature`; + +-- Banlist table has been migrated to the email_filter_rule table +DROP TABLE `%TABLE_PREFIX%email_banlist`; diff --git a/setup/inc/subscribe.inc.php b/setup/inc/subscribe.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..45dddf1d711d9f0e8795cc8bd9aea9fd891f7930 --- /dev/null +++ b/setup/inc/subscribe.inc.php @@ -0,0 +1,48 @@ +<?php if(!defined('SETUPINC')) die('Kwaheri!'); +$info=($_POST && $errors)?Format::htmlchars($_POST):$_SESSION['info']; +?> + <div id="main"> + <h1>Basic Installation Completed</h1> + <p>osTicket installation has been completed successfully.</p> + <h3 style="color:#FF7700;">Stay up to date: </h3> + It's important to keep your installation up to date. Get announcements, security updates and alerts delivered directly to you! + <br><br> + <form action="install.php" method="post"> + <input type="hidden" name="s" value="subscribe"> + <div class="row"> + <label>Full Name:</label> + <input type="text" name="name" size="30" value="<?php echo $info['name']; ?>"> + <font color="red"><?php echo $errors['name']; ?></font> + </div> + <div class="row"> + <label>Email Address:</label> + <input type="text" name="email" size="30" value="<?php echo $info['email']; ?>"> + <font color="red"><?php echo $errors['email']; ?></font> + </div> + <br> + <div class="row"> + <strong>I'd like to receive the following notifications: <font color="red"><?php echo $errors['notify']; ?></font></strong> + <label style="width:500px"> + <input style="position:relative; top:4px; margin-right:10px" + type="checkbox" name="news" value="1" <?php echo (!isset($info['news']) || $info['news'])?'checked="checked"':''; ?> > + News & Announcements</label> + <label style="width:500px"> + <input style="position:relative; top:4px; margin-right:10px" + type="checkbox" name="alerts" value="1" <?php echo (!isset($info['alerts']) || $info['alerts'])?'checked="checked"':''; ?>> + Security Alerts</label> + </div> + <div id="bar"> + <input class="btn" type="submit" value="Keep me Updated"> + <a class="unstyled" href="install.php?s=ns">No thanks.</a> + </div> + </form> + </div> + <div id="sidebar"> + <h3>Thank you!</h3> + <p> + Once again, thank you for choosing osTicket as your new customer support platform! + </p> + <p> + Launching a new customer support platform can be a daunting task. Let us get you started! We provide professional support services to help get osTicket up and running smoothly for your organization. <a target="_blank" href="http://osticket.com/support/professional_services.php">Learn More!</a> + </p> + </div> diff --git a/setup/inc/upgrade-aborted.inc.php b/setup/inc/upgrade-aborted.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..8d8f64aa831841dd9431f017c7289760ba18448e --- /dev/null +++ b/setup/inc/upgrade-aborted.inc.php @@ -0,0 +1,27 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +?> +<div id="main"> + <h1 style="color:#FF7700;">Upgrade Aborted!</h1> + <div id="intro"> + <p>Upgrade aborted due to errors. Any errors at this stage are fatal. Please note the error(s), if any, when contacting us.<p> + <?php + if($_SESSION['upgrader']['errors']) { + $errors=$_SESSION['upgrader']['errors']; + if($errors['err']) + echo sprintf('<b><font color="red">%s</font></b>',$errors['err']); + + echo '<ul class="error">'; + unset($errors['err']); + foreach($errors as $k=>$error) + echo sprintf('<li>%s</li>',$error); + echo '</ul>'; + } ?> + <p>Please, refer to the <a target="_blank" href="http://osticket.com/wiki/Upgrade_and_Migration">Upgrade Guide</a> on the wiki for more information.</p> + </div> + <p><strong>Need Help?</strong> We provide <a target="_blank" href="http://osticket.com/support/professional_services.php"><u>professional upgrade services</u></a> and commercial support. <a target="_blank" href="http://osticket.com/support/">Contact us</a> today for expedited help.</p> +</div> +<div id="sidebar"> + <h3>What to do?</h3> + <p>Restore your previous version from backup and try again or <a target="_blank" href="http://osticket.com/support/">seek help</a>.</p> +</div> diff --git a/setup/inc/upgrade-attachments.inc.php b/setup/inc/upgrade-attachments.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..1a69526454fa6e695b6d50c3f9b305acafd110d8 --- /dev/null +++ b/setup/inc/upgrade-attachments.inc.php @@ -0,0 +1,32 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); +$msg = $_SESSION['upgrader']['msg']; +?> +<div id="main"> + <h1>Attachments Migration</h1> + <p>We're almost done! We're now migrating attachments to the database, it might take a while dending on the number of files in your database.<p> + <p style="color:#FF7700;font-weight:bold;">We have to migrate files in batches for technical reasons.</p> + <p>Please don't cancel or close the browser.</p> + + <div id="bar"> + <form method="post" action="upgrade.php" id="attachments"> + <input type="hidden" name="s" value="cleanup"> + <input class="btn" type="submit" name="submit" value="Next Batch"> + </form> + + </div> +</div> +<div id="sidebar"> + <h3>Upgrade Tips</h3> + <p>1. Be patient the process will take a few minutes.</p> + <p>2. If you experience any problems, you can always restore your files/dabase backup.</p> + <p>3. We can help, feel free to <a href="http://osticket.com/support/" target="_blank">contact us </a> for professional help.</p> +</div> +<div id="overlay"></div> + <div id="loading"> + <h4>Moving attachments</h4> + <br> + Please wait... while we migrate attachments! + <br><br> + <div id="msg" style="font-weight: bold;"><?php echo Format::htmlchars($msg); ?></div> + </div> diff --git a/setup/inc/upgrade-cleanup.inc.php b/setup/inc/upgrade-cleanup.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..f8406ae533a9efbce73e864239d749b586e8f66e --- /dev/null +++ b/setup/inc/upgrade-cleanup.inc.php @@ -0,0 +1,38 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); + +?> + <div id="main"> + <h1>osTicket Upgrade</h1> + <div id="intro"> + <p>We're almost done! Please don't cancel or close the browser, any errors at this stage will be fatal.</p> + </div> + <h2>Cleanup: Step 2 of 2</h2> + <p>The upgrade wizard will now attempt to do post upgrade cleanup. It might take a while dending on the size of your database. </p> + <ul> + <li>Setting Changes</li> + <li>Attachment Migration</li> + <li>Database Optimization</li> + </ul> + <div id="bar"> + <form method="post" action="upgrade.php" id="cleanup"> + <input type="hidden" name="s" value="cleanup"> + <input class="btn" type="submit" name="submit" value="Do It Now!"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Upgrade Tips</h3> + <p>1. Be patient the process will take a couple of seconds.</p> + <p>2. If you experience any problems, you can always restore your files/dabase backup.</p> + <p>3. We can help, feel free to <a href="http://osticket.com/support/" target="_blank">contact us </a> for professional help.</p> + </div> + + <div id="overlay"></div> + <div id="loading"> + <h4>Doing serious stuff!</h4> + <br> + Please wait... while we do post-upgrade cleanup! + <br><br> + <div id="msg" style="font-weight: bold;"></div> + </div> diff --git a/setup/inc/upgrade-core.inc.php b/setup/inc/upgrade-core.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..ec42e50437537ea01931484a18e86c902ba909b1 --- /dev/null +++ b/setup/inc/upgrade-core.inc.php @@ -0,0 +1,38 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); + +?> + <div id="main"> + <h1>osTicket Upgrade</h1> + <div id="intro"> + <p>Thank you for taking the time to upgrade your osTicket intallation!</p> + <p>Please don't cancel or close the browser, any errors at this + stage will be fatal.</p> + </div> + <h2>Base upgrade: Step 1 of 2</h2> + <p>The upgrade wizard will now attempt to upgrade your database and core settings!</p> + <ul> + <li>Database enhancements</li> + <li>New and updated features</li> + <li>Enhance settings and security</li> + </ul> + <div id="bar"> + <form method="post" action="upgrade.php" id="upgrade"> + <input type="hidden" name="s" value="upgrade"> + <input class="btn" type="submit" name="submit" value="Do It Now!"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Upgrade Tips</h3> + <p>1. Be patient the process will take a couple of seconds.</p> + <p>2. If you experience any problems, you can always restore your files/dabase backup.</p> + <p>3. We can help, feel free to <a href="http://osticket.com/support/" target="_blank">contact us </a> for professional help.</p> + </div> + + <div id="overlay"></div> + <div id="loading"> + <h4>Doing serious stuff!</h4> + Please wait... while we upgrade your osTicket installation! + <div id="udb"><br><b>Smile!</b></div> + </div> diff --git a/setup/inc/upgrade-done.inc.php b/setup/inc/upgrade-done.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0ec75c31a8099d5c0877231a00e274400a97f049 --- /dev/null +++ b/setup/inc/upgrade-done.inc.php @@ -0,0 +1,30 @@ +<?php if(!defined('SETUPINC')) die('Kwaheri!'); +$url=URL; + +?> + <div id="main"> + <h1 style="color:green;">Upgrade Completed!</h1> + <div id="intro"> + <p>Congratulations osTicket upgrade has been completed successfully. Please refer to Release Notes for more information about changes and/or new features.</p> + <h3 style="color:#FF7700;">Post-upgrade manual cleanup</h3> + Please take a minute to cleanup! + <ul> + <li><b>Delete setup directory</b>:<br> After verifying that the upgrade completed correctly please delete setup folder.</li> + <li><b>Enable the helpdesk</b>:<br> You can now enable osTicket, be sure to take some time to explore changes and new features</li> + </ul> + </div> + <p>Once again, thank you for choosing osTicket. Your feedback is welcomed!</p> + <p>We take user feedback seriously and are dedicated to making changes based on your input. Feel free to let us know of any other improvements you would like to see in osTicket, so that we may add them in the future as we continue to develop better and better versions of osTicket.</p> + <p>Good luck.<p> + <p>osTicket Team.</p> + <br> + <p><b>PS</b>: Don't just make customers happy, make happy customers!</p> + </div> + <div id="sidebar"> + <h3>What's Next?</h3> + <p><b>Post-upgrade</b>: You can now log in to <a href="../scp/admin.php" target="_blank">Admin Panel</a> and explore the new features. For complete and upto date release notes see <a href="http://osticket.com/wiki/Post-Install_Setup_Guide" target="_blank">osTicket wiki</a></p> + + <p><b>Stay up to date</b>: It's important to keep your osTicket installation up to date. Get announcements, security updates and alerts delivered directly to you! + <a target="_blank" href="http://osticket.com/support/subscribe.php">Subcribe today</a> and be in the loop!</p> + <p><b>Commercial support available</b>: Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely. <a target="_blank" href="http://osticket.com/support/commercial_support.php.php">Learn More!</a></p> + </div> diff --git a/setup/inc/upgrade.inc.php b/setup/inc/upgrade.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..e5b8ba77bef7da18c547820031d1ca294b2b4dea --- /dev/null +++ b/setup/inc/upgrade.inc.php @@ -0,0 +1,50 @@ +<?php +if(!defined('SETUPINC')) die('Kwaheri!'); + +?> + <div id="main"> + <h1>osTicket Upgrade!</h1> + <font color="red"><b><?php echo $errors['err']; ?></b></font> + <div id="intro"> + <p>Thank you for being a loyal osTicket user!</p> + <p>The upgrade wizard will guide you every step of the way in the upgrade process. While we try to ensure that the upgrade process is straightforward and painless, we can't guarantee it will be the case for every user.</p> + </div> + <h2>Getting ready!</h2> + <p>Before we begin, we'll check your server configuration to make sure you meet the minimum requirements to run the latest version of osTicket.</p> + <h3>Prerequisites: <font color="red"><?php echo $errors['prereq']; ?></font></h3> + These items are necessary in order to use osTicket the latest version of osTicket. + <ul class="progress"> + <li class="<? echo $upgrader->check_php()?'yes':'no'; ?>"> + PHP v4.3 or greater - (<small><b><?php echo PHP_VERSION; ?></b></small>)</li> + <li class="<? echo $upgrader->check_mysql()?'yes':'no'; ?>"> + MySQL v4.4 or greater - (<small><b><?php echo extension_loaded('mysql')?'module loaded':'missing!'; ?></b></small>)</li> + </ul> + <h3>Higly Recommended:</h3> + We hightly recommend that you follow the steps below. + <ul> + <li>Take osTicket offline momentarily during upgrade.</li> + <li>Backup the current database, if you haven't done so already.</li> + <li>Be patient the upgrade process will take a couple of seconds.</li> + </ul> + <div id="bar"> + <form method="post" action="upgrade.php" id="upgrade"> + <input type="hidden" name="s" value="prereq"> + <input class="btn" type="submit" name="submit" value="Start Upgrade Now »"> + </form> + </div> + </div> + <div id="sidebar"> + <h3>Upgrade Tips</h3> + <p>1. Remember to backup your osTicket database</p> + <p>2. Take osTicket offline momentarily</p> + <p>3. If you experience any problems, you can always restore your files/dabase backup.</p> + <p>4. We can help, feel free to <a href="http://osticket.com/support/" target="_blank">contact us </a> for professional help.</p> + + </div> + + <div id="overlay"></div> + <div id="loading"> + <h4>Doing stuff!</h4> + Please wait... while we upgrade your osTicket installation! + <div id="udb"></div> + </div> diff --git a/setup/index.php b/setup/index.php new file mode 100644 index 0000000000000000000000000000000000000000..36664d8a7ce83dcf2fe41dc4720569c92606344d --- /dev/null +++ b/setup/index.php @@ -0,0 +1,3 @@ +<?php +require('install.php'); +?> diff --git a/setup/install.php b/setup/install.php new file mode 100644 index 0000000000000000000000000000000000000000..e98c7aa7bf4791a3c2dd536b4722e3d4206a4a80 --- /dev/null +++ b/setup/install.php @@ -0,0 +1,111 @@ +<?php +/********************************************************************* + install.php + + osTicket Installer. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('setup.inc.php'); + +//define('OSTICKET_CONFIGFILE','../include/ost-config.php'); //osTicket config file full path. +define('OSTICKET_CONFIGFILE','../include/ost-config.php'); //XXX: Make sure the path is corrent b4 releasing. + + +$installer = new Installer(OSTICKET_CONFIGFILE); //Installer instance. +$wizard=array(); +$wizard['title']='osTicket Installer'; +$wizard['tagline']='Installing osTicket v'.$installer->getVersionVerbose(); +$wizard['logo']='logo.png'; +$wizard['menu']=array('Installation Guide'=>'http://osticket.com/wiki/Installation', + 'Get Professional Help'=>'http://osticket.com/support'); + +if($_POST && $_POST['s']) { + $errors = array(); + $_SESSION['installer']['s']=$_POST['s']; + switch(strtolower($_POST['s'])) { + case 'prereq': + if($installer->check_prereq()) + $_SESSION['installer']['s']='config'; + else + $errors['prereq']='Minimum requirements not met!'; + break; + case 'config': + if(!$installer->config_exists()) + $errors['err']='Configuratin file does NOT exist. Follow steps below'.$installer->getConfigFile(); + elseif(!$installer->config_writable()) + $errors['err']='Write access required to continue'; + else + $_SESSION['installer']['s']='install'; + break; + case 'install': + if($installer->install($_POST)) { + $_SESSION['info']=array('name' =>ucfirst($_POST['fname'].' '.$_POST['lname']), + 'email' =>$_POST['admin_email'], + 'URL'=>URL); + //TODO: Go to subscribe step. + $_SESSION['installer']['s']='done'; + } elseif(!($errors=$installer->getErrors()) || !$errors['err']) { + $errors['err']='Error installing osTicket - correct the errors below and try again.'; + } + break; + case 'subscribe': + if(!trim($_POST['name'])) + $errors['name'] = 'Required'; + + if(!$_POST['email']) + $errors['email'] = 'Required'; + elseif(!Validator::is_email($_POST['email'])) + $errors['email'] = 'Invalid'; + + if(!$_POST['alerts'] && !$_POST['news']) + $errors['notify'] = 'Check one or more'; + + if(!$errors) + $_SESSION['installer']['s'] = 'done'; + break; + } + +}elseif($_GET['s'] && $_GET['s']=='ns' && $_SESSION['installer']['s']=='subscribe') { + $_SESSION['installer']['s']='done'; +} + + +switch(strtolower($_SESSION['installer']['s'])) { + case 'config': + case 'install': + if(!$installer->config_exists()) { + $inc='file-missing.inc.php'; + } elseif(!($cFile=file_get_contents($installer->getConfigFile())) + || preg_match("/define\('OSTINSTALLED',TRUE\)\;/i",$cFile)) { //osTicket already installed or empty config file? + $inc='file-unclean.inc.php'; + } elseif(!$installer->config_writable()) { //writable config file?? + clearstatcache(); + $inc='file-perm.inc.php'; + } else { //Everything checked out show install form. + $inc='install.inc.php'; + } + break; + case 'subscribe': //TODO: Prep for v1.7 RC1 + $inc='subscribe.inc.php'; + break; + case 'done': + $inc='install-done.inc.php'; + if(!$installer->config_exists()) + $inc='install-prereq.inc.php'; + break; + default: + $inc='install-prereq.inc.php'; +} + +require(INC_DIR.'header.inc.php'); +require(INC_DIR.$inc); +require(INC_DIR.'footer.inc.php'); +?> diff --git a/setup/js/jquery-1.6.2.min.js b/setup/js/jquery-1.6.2.min.js new file mode 100644 index 0000000000000000000000000000000000000000..48590ecb96a74f5987d125e7fbc5a26c1392543a --- /dev/null +++ b/setup/js/jquery-1.6.2.min.js @@ -0,0 +1,18 @@ +/*! + * jQuery JavaScript Library v1.6.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Jun 30 14:16:56 2011 -0400 + */ +(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bZ(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bY(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bC.test(a)?d(a,e):bY(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bY(a+"["+e+"]",b[e],c,d);else d(a,b)}function bX(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bR,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bX(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bX(a,c,d,e,"*",g));return l}function bW(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bN),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bA(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bv:bw;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bg(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(H)return H.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test("Â ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),h&&f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,l.done(m);m();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,u=/\:|^on/,v,w;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(o);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(o);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(n," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h<i;h++){var j=e[h];if(j.selected&&(f.support.optDisabled?!j.disabled:j.getAttribute("disabled")===null)&&(!j.parentNode.disabled||!f.nodeName(j.parentNode,"optgroup"))){b=f(j).val();if(g)return b;d.push(b)}}if(g&&!d.length&&e.length)return f(e[c]).val();return d},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i. +shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget,c=!1,d=a.type;a.type=a.data,b!==this&&(b&&(c=f.contains(this,b)),c||(f.event.handle.apply(this,arguments),a.type=d))},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){if(a===b){g=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be=/^\s*<!(?:\[CDATA\[|\-\-)/,bf={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bg(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bm)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i;b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j +)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1></$2>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bl(k[i]);else bl(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bn=/alpha\([^)]*\)/i,bo=/opacity=([^)]*)/,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c)),h="number"),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bA(a,b,d);f.swap(a,bu,function(){e=bA(a,b,d)});return e}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cs(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cr("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cr("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=cs(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block"))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],cj.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=ck.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[i]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cr("show",1),slideUp:cr("hide",1),slideToggle:cr("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=cn||cp(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cl&&(co?(cl=!0,g=function(){cl&&(co(g),e.tick())},co(g)):cl=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=cn||cp(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b<a.length;++b)a[b]()||a.splice(b--,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cl),cl=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var ct=/^t(?:able|d|h)$/i,cu=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cv(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!ct.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/setup/js/setup.js b/setup/js/setup.js new file mode 100644 index 0000000000000000000000000000000000000000..5d274ea3198bbec310c20abef16ac68df51ee270 --- /dev/null +++ b/setup/js/setup.js @@ -0,0 +1,63 @@ +jQuery(function($) { + + $("#overlay").css({ + opacity : 0.3, + top : 0, + left : 0, + width : $(window).width(), + height : $(window).height() + }); + + $("#loading").css({ + top : ($(window).height() / 3), + left : ($(window).width() / 2 - 160) + }); + + $('form#install, form#upgrade, form#attachments').submit(function(e) { + $('input[type=submit]', this).attr('disabled', 'disabled'); + $('#overlay, #loading').show(); + return true; + }); + + $('form#cleanup').submit(function(e) { + e.preventDefault(); + var form = $(this); + $('input[type=submit]', this).attr('disabled', 'disabled'); + $('#overlay, #loading').show(); + doCleanup('upgrade',form.attr('action')); + return false; + }); + + + function doCleanup(type,url) { + function _lp(count) { + $.ajax({ + type: 'GET', + url: 'cleanup.php', + async: true, + cache: false, + data: {c:count,type:type}, + dataType: 'text', + success: function(res) { + if (res) { + $('#loading #msg').html(res); + } + }, + statusCode: { + 200: function() { + setTimeout(function() { _lp(count+1); },2); + }, + + 304: function() { + $('#loading #msg').html("We're done... "); + setTimeout(function() { location.href =url;},1000); + } + }, + error: function(){ + alert("Something went wrong"); + } + }); + }; + _lp(0); + } +}); diff --git a/setup/js/tips.js b/setup/js/tips.js new file mode 100644 index 0000000000000000000000000000000000000000..f24d82512d6b0bd6258ae8c674bce77bfaed0826 --- /dev/null +++ b/setup/js/tips.js @@ -0,0 +1,60 @@ +jQuery(function($) { + var tips = $('.tip'); + for(i=0;i<tips.length;i++) { + tips[i].rel = 'tip-' + i; + } + + $('.tip').live('mouseover click', function(e) { + e.preventDefault(); + var tip_num = this.rel; + + if($('.' + tip_num).length == 0) { + + var elem = $(this); + var pos = elem.offset(); + + var y_pos = pos.top - 12; + var x_pos = pos.left + elem.width() + 20; + + var tip_arrow = $('<img>').attr('src', './images/tip_arrow.png').addClass('tip_arrow'); + var tip_box = $('<div>').addClass('tip_box'); + var tip_content = $('<div>').addClass('tip_content').load('tips.html '+elem.attr('href'), function() { + tip_content.prepend('<a href="#" class="tip_close">x</a>'); + }); + + var the_tip = tip_box.append(tip_arrow).append(tip_content); + the_tip.css({ + "top":y_pos + "px", + "left":x_pos + "px" + }).addClass(tip_num); + + tip_timer = setTimeout(function() { + $('.tip_box').remove(); + $('body').append(the_tip.hide().fadeIn()); + }, 500); + + $('.' + tip_num + ' .tip_shadow').css({ + "height":$('.' + tip_num).height() + 5 + }); + } + }).live('mouseout', function() { + clearTimeout(tip_timer); + }); + $('body').delegate('.tip_close', 'click', function(e) { + e.preventDefault(); + $(this).parent().parent().remove(); + }).delegate('.tip_menu .assign', 'click', function(e) { + e.preventDefault(); + elem = $(this).parent().parent().parent().parent(); + $('.tip_body', elem).slideToggle(function() { + $('.assign_panel', elem).slideToggle(); + }); + }).delegate('.assign_panel .cancel', 'click', function(e) { + e.preventDefault(); + elem = $(this).parent().parent().parent(); + $('.assign_panel', elem).slideToggle(function() { + $('.tip_body', elem).slideToggle(); + }); + }); + +}); diff --git a/setup/setup.inc.php b/setup/setup.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..7fc66597717bbcf4ef7a092b25329d2460e1ece4 --- /dev/null +++ b/setup/setup.inc.php @@ -0,0 +1,53 @@ +<?php +/********************************************************************* + setup.inc.php + + Master include file for setup/install scripts. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +#inits +error_reporting(E_ALL ^ E_NOTICE); //turn on errors +ini_set('magic_quotes_gpc', 0); +ini_set('session.use_trans_sid', 0); +ini_set('session.cache_limiter', 'nocache'); +ini_set('display_errors',1); //We want the user to see errors during install process. +ini_set('display_startup_errors',1); + +#start session +session_start(); + +#clear global vars +$errors=array(); +$msg=''; + +#define constants. +define('SETUPINC',true); +define('URL',rtrim('http'.(($_SERVER['HTTPS']=='on')?'s':'').'://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['PHP_SELF']),'setup')); + +#define paths +define('INC_DIR','./inc/'); //local include dir! +if(!defined('INCLUDE_DIR')): +define('ROOT_PATH','../'); +define('ROOT_DIR','../'); +define('INCLUDE_DIR',ROOT_DIR.'include/'); +define('PEAR_DIR',INCLUDE_DIR.'pear/'); +ini_set('include_path', './'.PATH_SEPARATOR.INC_DIR.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR); +endif; + +#required files +require_once(INC_DIR.'class.setup.php'); +require_once(INCLUDE_DIR.'class.validator.php'); +require_once(INCLUDE_DIR.'class.format.php'); +require_once(INCLUDE_DIR.'class.misc.php'); +require_once(INCLUDE_DIR.'mysql.php'); + +?> diff --git a/setup/tips.html b/setup/tips.html new file mode 100644 index 0000000000000000000000000000000000000000..1cd8de936c37b24139da914f08889d49ad8b01ae --- /dev/null +++ b/setup/tips.html @@ -0,0 +1,52 @@ +<div id="t1"> +<b>Helpdesk Name</b> +<p>The name of your support system e.g [Company Name] Support</p> +</div> +<div id="t2"> +<b>Default System Email</b> +<p>Default email address e.g support@yourcompany.com - you can add more later!</p> +</div> +<div id="t3"> +<b>First Name</b> +<p>Admin's first name</p> +</div> +<div id="t4"> +<b>Last Name</b> +<p>Admin's last name</p> +</div> +<div id="t5"> +<b>Email Address</b> +<p>Admin's personal email address. Must be different from system's default email.</p> +</div> +<div id="t6"> +<b>Username</b> +<p>Admin's login name. Must be at least three (3) characters.</p> +</div> +<div id="t7"> +<b>Password</b> +<p>Admin's password. Must be five (5) characters or more.</p> +</div> +<div id="t8"> +<b>Confirm Password</b> +<p>Retype admin's password. Must match.</p> +</div> +<div id="t9"> +<b>MySQL Table Prefix.</b> +<p>osTicket requires table prefix in order to avoid possible table conflicts in a shared database.</p> +</div> +<div id="t10"> +<b>MySQL Hostname</b> +<p>Most hosts use 'localhost' for local database hostname. Check with your host if localhost fails. Default port set in php.ini is assumed.</p> +</div> +<div id="t11"> +<b>MySQL Database</b> +<p>Name of the database osTicket will use.</p> +</div> +<div id="t12"> +<b>MySQL Username</b> +<p>The MySQL user must have full rights to the database.</p> +</div> +<div id="t13"> +<b>MySQL Password</b> +<p>MySQL password associated with above user.</p> +</div> diff --git a/setup/upgrade.php b/setup/upgrade.php new file mode 100644 index 0000000000000000000000000000000000000000..6fc34cfd307cfcc9df319deb25101ec159f52d75 --- /dev/null +++ b/setup/upgrade.php @@ -0,0 +1,17 @@ +<?php +/********************************************************************* + upgrade.php + + osTicket Upgrade Wizard + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +die('Upgrade NOT supported for v1.7 DPR.'); +?>