diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3303e4bd --- /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 00000000..d8cf7d46 --- /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 00000000..6ff6cf1b --- /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 00000000..d209860d --- /dev/null +++ b/api/api.inc.php @@ -0,0 +1,85 @@ + + 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 00000000..86de02cd --- /dev/null +++ b/api/cron.php @@ -0,0 +1,21 @@ + + 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 00000000..646df679 --- /dev/null +++ b/api/http.php @@ -0,0 +1,27 @@ +resolve($_SERVER['PATH_INFO']); + +?> diff --git a/api/index.php b/api/index.php new file mode 100644 index 00000000..f9518a67 --- /dev/null +++ b/api/index.php @@ -0,0 +1,3 @@ + diff --git a/api/pipe.php b/api/pipe.php new file mode 100644 index 00000000..b4e5e5ad --- /dev/null +++ b/api/pipe.php @@ -0,0 +1,132 @@ +#!/usr/bin/php -q + + 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 00000000..b6a9555e --- /dev/null +++ b/api/urls.conf.php @@ -0,0 +1,11 @@ +xml|json)$", "create") +); + +?> diff --git a/assets/default/css/print.css b/assets/default/css/print.css new file mode 100644 index 00000000..9f1c2574 --- /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 00000000..dde40189 --- /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 00000000..b0856af9 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 00000000..cf89d50a 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 00000000..9ecfda98 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 00000000..ea8e68ac 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 00000000..8892a55a 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 00000000..4400e61e 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 00000000..a11afcfd 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 00000000..7625feaa 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 00000000..c373e787 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 00000000..9d8e7c72 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 00000000..75564839 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 00000000..7a5d21fd 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 00000000..c24bf877 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 00000000..8268958a 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 00000000..ba6dd226 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 00000000..4c211878 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 00000000..bffd6b0b 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 00000000..2b4b91d5 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 00000000..20eff712 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 00000000..2b4b91d5 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 00000000..44bf451c 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 00000000..25ec9db8 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 00000000..d66c07f6 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 00000000..f63f93fa 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 00000000..d619684d 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 00000000..3b021740 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 00000000..db3196a0 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 00000000..08bed66b 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 00000000..9cd429e6 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 00000000..49445a45 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 00000000..18cfcdd4 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 00000000..53ae09f6 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 00000000..5461bd2f --- /dev/null +++ b/client.inc.php @@ -0,0 +1,65 @@ + + 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 00000000..e20f3177 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 00000000..8526cf5d 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 00000000..229fe90b 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 00000000..a029c420 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 00000000..ca80322d 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 00000000..c31029e4 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 00000000..2e517f2a 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 00000000..a6e768f1 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 00000000..afd9f442 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 00000000..fd1f5cab 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 00000000..8e7d520e 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 00000000..aa29fd1b 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 00000000..7ad3ac27 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 00000000..b25c0781 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 00000000..4400e61e 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 00000000..8268958a 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 00000000..bffd6b0b 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 00000000..4304ea79 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 00000000..6b57605d 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 00000000..4304ea79 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 00000000..b9aa8ed5 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 00000000..1b7a5b90 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 00000000..feb6a95f 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 00000000..25634413 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 00000000..16bc12ed 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 00000000..6dd774f0 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 00000000..ee2d25bb 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 00000000..32d9636e 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 00000000..855eef06 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 00000000..c98eb7d6 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 00000000..c08f52ed 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 00000000..8a33b22d 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 00000000..07755499 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 00000000..bf27b1f9 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 00000000..2678913d 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 00000000..6cd8f080 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 00000000..8ed6be9c 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 00000000..e75eba65 --- /dev/null +++ b/include/JSON.php @@ -0,0 +1,805 @@ + + * @author Matt Knapp + * @author Brett Stimmerman + * @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: + * + * + * // 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); + * + */ +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 00000000..a30f4113 --- /dev/null +++ b/include/PasswordHash.php @@ -0,0 +1,253 @@ + 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 00000000..7dcfd997 --- /dev/null +++ b/include/ajax.config.php @@ -0,0 +1,32 @@ + + 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 00000000..e77f45d2 --- /dev/null +++ b/include/ajax.content.php @@ -0,0 +1,83 @@ + + 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('
 %s

%s

+
Log Date: %s IP Address: %s
', + $log->getTitle(), + Format::display(str_replace(',',', ',$log->getText())), + Format::db_daydatetime($log->getCreateDate()), + $log->getIP()); + }else { + $content='
 Error:Unknown or invalid log ID
'; + } + + return $content; + } + + function ticket_variables() { + + $content='
+

Ticket Variables

+ Please note that non-base variables depends on the context of use. +
+ + + + + + +
Base VariablesOther Variables
+ + + + + + + + + + + + + + + +
%idTicket ID (internal ID)
%ticketTicket number (external ID)
%emailEmail address
%nameFull name
%subjectSubject
%topicHelp topic (web only)
%phonePhone number | ext
%statusStatus
%priorityPriority
%deptDepartment
%assignedAssigned staff or team (if any)
%createdateDate created
%duedateDue date
%closedateDate closed
+
+ + + + + + + + + +
%messageMessage (incoming)
%responseResponse (outgoing)
%noteInternal/transfer note
%staffStaff\'s name (alert/notices)
%assigneeAssigned staff
%assignerStaff assigning the ticket
%urlosTicket\'s base url (FQDN)
+
+
'; + + return $content; + } +} +?> diff --git a/include/ajax.kbase.php b/include/ajax.kbase.php new file mode 100644 index 00000000..b51d8781 --- /dev/null +++ b/include/ajax.kbase.php @@ -0,0 +1,83 @@ + + 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( + '
+ %s

%s

+
Last updated %s
+
+ View | Attachments (%s)', + $faq->getQuestion(), + Format::safe_html($faq->getAnswer()), + Format::db_daydatetime($faq->getUpdateDate()), + $faq->getId(), + $faq->getId(), + $faq->getNumAttachments()); + if($thisstaff && $thisstaff->canManageFAQ()) { + $resp.=sprintf(' | Edit',$faq->getId()); + + } + $resp.='
'; + + return $resp; + } +} +?> diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php new file mode 100644 index 00000000..2eff8924 --- /dev/null +++ b/include/ajax.tickets.php @@ -0,0 +1,164 @@ + + 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( + '
+ Ticket #%d Preview
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.='
    '; + foreach($options as $option) { + $resp.=sprintf('
  • %s
  • ', + $option['url'],$option['action']); + } + $resp.='
'; + } + + $resp.='
'; + + return $resp; + } +} +?> diff --git a/include/api.ticket.php b/include/api.ticket.php new file mode 100644 index 00000000..d767e212 --- /dev/null +++ b/include/api.ticket.php @@ -0,0 +1,71 @@ + 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 00000000..742f224f --- /dev/null +++ b/include/class.ajax.php @@ -0,0 +1,53 @@ + 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 00000000..1bd25463 --- /dev/null +++ b/include/class.api.php @@ -0,0 +1,284 @@ + + 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 00000000..8644d671 --- /dev/null +++ b/include/class.attachment.php @@ -0,0 +1,103 @@ + + 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 00000000..27729b44 --- /dev/null +++ b/include/class.banlist.php @@ -0,0 +1,66 @@ + + 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 00000000..00c46de3 --- /dev/null +++ b/include/class.canned.php @@ -0,0 +1,272 @@ + + 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 00000000..1423735e --- /dev/null +++ b/include/class.captcha.php @@ -0,0 +1,54 @@ + + 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 00000000..2949b9a9 --- /dev/null +++ b/include/class.category.php @@ -0,0 +1,167 @@ +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 00000000..f7ede15c --- /dev/null +++ b/include/class.client.php @@ -0,0 +1,93 @@ + + 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 00000000..1e3d7745 --- /dev/null +++ b/include/class.config.php @@ -0,0 +1,841 @@ + + 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 00000000..3fd2d652 --- /dev/null +++ b/include/class.cron.php @@ -0,0 +1,43 @@ + + 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 00000000..8bd57511 --- /dev/null +++ b/include/class.dept.php @@ -0,0 +1,318 @@ + + 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 00000000..507b2f9f --- /dev/null +++ b/include/class.dispatcher.php @@ -0,0 +1,189 @@ +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 00000000..0b4317f7 --- /dev/null +++ b/include/class.email.php @@ -0,0 +1,461 @@ + + 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']='
'.$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']='
'.$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 00000000..63b0848e --- /dev/null +++ b/include/class.faq.php @@ -0,0 +1,356 @@ +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('(%s)',Format::file_size($attachment['size'])); + + $str.=sprintf('%s%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 00000000..af38bfd5 --- /dev/null +++ b/include/class.file.php @@ -0,0 +1,234 @@ + + 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 00000000..9a1caf2b --- /dev/null +++ b/include/class.filter.php @@ -0,0 +1,756 @@ + + 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 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 00000000..0dbd9e14 --- /dev/null +++ b/include/class.format.php @@ -0,0 +1,211 @@ + + 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@:%_\+.~#?&;\/\/=]+)/','\\1', $text); + $text=preg_replace("/(^|[ \\n\\r\\t])(www\.([a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+)(\/[^\/ \\n\\r]*)*)/", + '\\1\\2', $text); + $text=preg_replace("/(^|[ \\n\\r\\t])([_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4})/",'\\1\\2', $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 00000000..1b21dce2 --- /dev/null +++ b/include/class.group.php @@ -0,0 +1,155 @@ + + 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 00000000..d134e6b8 --- /dev/null +++ b/include/class.http.php @@ -0,0 +1,54 @@ + + 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 00000000..f983c73e --- /dev/null +++ b/include/class.json.php @@ -0,0 +1,62 @@ +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 00000000..d8a64689 --- /dev/null +++ b/include/class.knowledgebase.php @@ -0,0 +1,153 @@ +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 00000000..caa1be71 --- /dev/null +++ b/include/class.lock.php @@ -0,0 +1,157 @@ + + 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 diff --git a/include/class.log.php b/include/class.log.php new file mode 100644 index 00000000..9a2394cf --- /dev/null +++ b/include/class.log.php @@ -0,0 +1,76 @@ + + 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 00000000..5f977759 --- /dev/null +++ b/include/class.mailfetch.php @@ -0,0 +1,419 @@ + + 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("
", "\n", $body); + $body=str_replace(array("
", "
", "
", "
"), "\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 00000000..88648367 --- /dev/null +++ b/include/class.mailparse.php @@ -0,0 +1,217 @@ + + 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("
", "\n", $body); + $body=str_replace(array("
", "
", "
", "
"), "\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 00000000..a6a97910 --- /dev/null +++ b/include/class.mcrypt.php @@ -0,0 +1,43 @@ + + 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 00000000..c9a5db03 --- /dev/null +++ b/include/class.misc.php @@ -0,0 +1,144 @@ + + 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(''; + $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 00000000..561fc963 --- /dev/null +++ b/include/class.nav.php @@ -0,0 +1,287 @@ + + 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 00000000..ccbae6e0 --- /dev/null +++ b/include/class.ostsession.php @@ -0,0 +1,113 @@ + + 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_expirettl; + } + + 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 00000000..b182db51 --- /dev/null +++ b/include/class.pagenate.php @@ -0,0 +1,129 @@ + + 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«"; + } + + for ($i=$start_loop; $i <= $stop_loop; $i++) { + $page = ($i - 1) * $this->limit; + if ($i == $this_page) { + $html .= "\n[$i]"; + } else { + $html .= "\n$i"; + } + } + if($stop_loop<$total_pages){ + $nextspan=($stop_loop+$displayed_span>$total_pages)?$total_pages-$displayed_span:$stop_loop+$displayed_span; + $html .= "\n»"; + } + + + + return $html; + } + +} +?> diff --git a/include/class.passwd.php b/include/class.passwd.php new file mode 100644 index 00000000..ef0cdac3 --- /dev/null +++ b/include/class.passwd.php @@ -0,0 +1,43 @@ + + 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 00000000..2ece6077 --- /dev/null +++ b/include/class.pop3.php @@ -0,0 +1,3 @@ + diff --git a/include/class.priority.php b/include/class.priority.php new file mode 100644 index 00000000..6fefe367 --- /dev/null +++ b/include/class.priority.php @@ -0,0 +1,92 @@ + + 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 00000000..9ea62b05 --- /dev/null +++ b/include/class.sla.php @@ -0,0 +1,159 @@ + + 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 00000000..bfb0b79d --- /dev/null +++ b/include/class.staff.php @@ -0,0 +1,653 @@ + + 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 00000000..ab10cd99 --- /dev/null +++ b/include/class.sys.php @@ -0,0 +1,107 @@ + + 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 00000000..7aada43a --- /dev/null +++ b/include/class.team.php @@ -0,0 +1,224 @@ + + 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 00000000..ef53d7ed --- /dev/null +++ b/include/class.template.php @@ -0,0 +1,392 @@ + + 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 00000000..edb9ac46 --- /dev/null +++ b/include/class.ticket.php @@ -0,0 +1,1949 @@ + + 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('(%s)',Format::file_size($attachment['size'])); + + $str.=sprintf('%s%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 duedatemarkOverdue()) + $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 00000000..59159c8c --- /dev/null +++ b/include/class.topic.php @@ -0,0 +1,207 @@ + + 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 00000000..9a8283e3 --- /dev/null +++ b/include/class.usersession.php @@ -0,0 +1,181 @@ + + 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 00000000..ea2dc62d --- /dev/null +++ b/include/class.validator.php @@ -0,0 +1,193 @@ + + 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 00000000..854f1823 --- /dev/null +++ b/include/class.xml.php @@ -0,0 +1,91 @@ +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 00000000..c944f831 --- /dev/null +++ b/include/client/faq.inc.php @@ -0,0 +1,31 @@ +isPublished()) die('Access Denied'); + +$category=$faq->getCategory(); + +?> +

Frequently Asked Questions

+ +
+getQuestion() ?> +
+
+
+

+getAnswer()); ?> +

+

+getNumAttachments()) { ?> +

Attachments: getAttachmentsLinks(); ?>
+ +
Help Topics: + getHelpTopics())?implode(', ',$topics):' '; ?> +
+

+
+
 Last updated getUpdateDate()); ?>
diff --git a/include/client/footer.inc.php b/include/client/footer.inc.php new file mode 100644 index 00000000..4b3d902c --- /dev/null +++ b/include/client/footer.inc.php @@ -0,0 +1,8 @@ +
+ + + + diff --git a/include/client/header.inc.php b/include/client/header.inc.php new file mode 100644 index 00000000..ad88a0c8 --- /dev/null +++ b/include/client/header.inc.php @@ -0,0 +1,58 @@ +getTitle())?$cfg->getTitle():'osTicket :: Support Ticket System'; +header("Content-Type: text/html; charset=UTF-8\r\n"); +?> + + + + + <?php echo Format::htmlchars($title); ?> + + + + + + + + + +
+ + + +
+ +
+
+ + + +
+ +
+ +
+ diff --git a/include/client/kb-category.inc.php b/include/client/kb-category.inc.php new file mode 100644 index 00000000..f166fb5b --- /dev/null +++ b/include/client/kb-category.inc.php @@ -0,0 +1,35 @@ +isPublic()) die('Access Denied'); + +?> +
+

Frequently Asked Questions

+
+
 
+
+
+
getName() ?>
+

+getDescription()); ?> +

+
+getId()) + .' GROUP BY faq.faq_id'; +if(($res=db_query($sql)) && db_num_rows($res)) { + echo '
+
    '; + while($row=db_fetch_array($res)) { + echo sprintf(' +
  1. %s
  2. ', + $row['faq_id'],Format::htmlchars($row['question'])); + } + echo '
+
'; +}else { + echo 'Category does not have any FAQs. Back To Index'; +} +?> diff --git a/include/client/knowledgebase.inc.php b/include/client/knowledgebase.inc.php new file mode 100644 index 00000000..0af99ba2 --- /dev/null +++ b/include/client/knowledgebase.inc.php @@ -0,0 +1,115 @@ + +

Frequently Asked Questions

+
+ + + + + + + + + +
+ + + + +
+ +
+
+
+
+Search Results
"; + if(($res=db_query($sql)) && ($num=db_num_rows($res))) { + echo '
'.$num.' FAQs matched your search criteria. +
    '; + while($row=db_fetch_array($res)) { + echo sprintf(' +
  1. %s
  2. ', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo '
+
'; + } else { + echo 'The search did not match any FAQs.'; + } +} 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 '
Click on the category to browse FAQs.
+
    '; + while($row=db_fetch_array($res)) { + + echo sprintf(' +
  • +

    %s (%d)

    + %s +
  • ',$row['category_id'], + Format::htmlchars($row['name']),$row['faqs'], + Format::safe_html($row['description'])); + } + echo '
'; + } else { + echo 'NO FAQs found'; + } +} +?> +
diff --git a/include/client/login.inc.php b/include/client/login.inc.php new file mode 100644 index 00000000..0ff10e3f --- /dev/null +++ b/include/client/login.inc.php @@ -0,0 +1,26 @@ + +

Check Ticket Status

+

To view the status of a ticket, provide us with the login details below.

+
+ Authentication Required +
+ + +
+
+ + +
+

+ +

+
+
+

+If this is your first time contacting us or you've lost the ticket ID, please open a new ticket. +

diff --git a/include/client/open.inc.php b/include/client/open.inc.php new file mode 100644 index 00000000..fd31bc9b --- /dev/null +++ b/include/client/open.inc.php @@ -0,0 +1,109 @@ + + +

Open a New Ticket

+

Please fill in the form below to open a new ticket.

+
+
+ + + +
+
+ + + +
+
+ + + + +     +
+
+
+ + + +
+
+ + + +
+
+ + + Please provide as much details as possible so we can best assist you. +
+
+ + +
+ allowOnlineAttachments() && !$cfg->allowAttachmentsOnlogin()) + || ($cfg->allowAttachmentsOnlogin() && ($thisuser && $thisuser->isValid()))) { ?> +
+ +   +
+ + 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)) {?> +
+ + +   +
+ + enableCaptcha() && (!$thisuser || !$thisuser->isValid())) { + if($_POST && $errors && !$errors['captcha']) + $errors['captcha']='Please re-enter the text again'; + ?> +
+
+ + + + Enter the text shown on the image. + +
+ +
+

+ + + +

+
diff --git a/include/client/thankyou.inc.php b/include/client/thankyou.inc.php new file mode 100644 index 00000000..6e2fe3b4 --- /dev/null +++ b/include/client/thankyou.inc.php @@ -0,0 +1,20 @@ + +
+ getName()); ?>,
+

+ Thank you for contacting us.
+ A support ticket request has been created and a representative will be getting back to you shortly if necessary.

+ + autoRespONNewTicket()){ ?> +

An email with the ticket number has been sent to getEmail(); ?>. + You'll need the ticket number along with your email to view status and progress online. +

+

+ If you wish to send additional comments or information regarding same issue, please follow the instructions on the email. +

+ +

Support Team

+
diff --git a/include/htmLawed.php b/include/htmLawed.php new file mode 100644 index 00000000..28d23482 --- /dev/null +++ b/include/htmLawed.php @@ -0,0 +1,711 @@ +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('``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 ''; 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 .= "";} + } + echo $add, ''; 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 ''; 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 = ""; + for(;++$k<$kc;){$add = "{$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 '
', $x, '
'; + } + 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 '';} +$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]) ? "" : (($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(' ]*(?)\s+`', '`\s+`', '`(<\w[^>]*(?) `'), array(' $1', ' ', '$1'), preg_replace_callback(array('`(<(!\[CDATA\[))(.+?)(\]\]>)`sm', '`(<(!--))(.+?)(-->)`sm', '`(<(pre|script|textarea)[^>]*?>)(.+?)()`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 00000000..f9518a67 --- /dev/null +++ b/include/index.php @@ -0,0 +1,3 @@ + diff --git a/include/mysql.php b/include/mysql.php new file mode 100644 index 00000000..192859af --- /dev/null +++ b/include/mysql.php @@ -0,0 +1,209 @@ + + 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 00000000..8a1f3b98 --- /dev/null +++ b/include/ost-sampleconfig.php @@ -0,0 +1,47 @@ + + 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 00000000..45a3f713 --- /dev/null +++ b/include/pear/Auth/SASL.php @@ -0,0 +1,99 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: SASL.php,v 1.5 2006/03/22 05:20:11 amistry Exp $ + +/** +* Client implementation of various SASL mechanisms +* +* @author Richard Heyes +* @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 00000000..dc6511c1 --- /dev/null +++ b/include/pear/Auth/SASL/Anonymous.php @@ -0,0 +1,71 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: Anonymous.php,v 1.4 2003/02/21 16:07:17 mj Exp $ + +/** +* Implmentation of ANONYMOUS SASL mechanism +* +* @author Richard Heyes +* @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 00000000..f21c8cc1 --- /dev/null +++ b/include/pear/Auth/SASL/Common.php @@ -0,0 +1,74 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: Common.php,v 1.6 2003/02/21 16:07:17 mj Exp $ + +/** +* Common functionality to SASL mechanisms +* +* @author Richard Heyes +* @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 00000000..086248fe --- /dev/null +++ b/include/pear/Auth/SASL/CramMD5.php @@ -0,0 +1,68 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: CramMD5.php,v 1.4 2003/02/21 16:07:17 mj Exp $ + +/** +* Implmentation of CRAM-MD5 SASL mechanism +* +* @author Richard Heyes +* @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 + * " ". 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 00000000..4534e500 --- /dev/null +++ b/include/pear/Auth/SASL/DigestMD5.php @@ -0,0 +1,198 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: DigestMD5.php,v 1.8 2006/03/22 05:20:11 amistry Exp $ + +/** +* Implmentation of DIGEST-MD5 SASL mechanism +* +* @author Richard Heyes +* @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-]+)=("[^"]+(? diff --git a/include/pear/Auth/SASL/Login.php b/include/pear/Auth/SASL/Login.php new file mode 100644 index 00000000..bad9ab98 --- /dev/null +++ b/include/pear/Auth/SASL/Login.php @@ -0,0 +1,65 @@ + | +// +-----------------------------------------------------------------------+ +// +// $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 +* @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 00000000..e4662ff7 --- /dev/null +++ b/include/pear/Auth/SASL/Plain.php @@ -0,0 +1,63 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: Plain.php,v 1.6 2003/09/11 18:53:56 mbretter Exp $ + +/** +* Implmentation of PLAIN SASL mechanism +* +* @author Richard Heyes +* @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 00000000..7a3c70f8 --- /dev/null +++ b/include/pear/Mail.php @@ -0,0 +1,245 @@ + | +// +----------------------------------------------------------------------+ +// +// $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('=((||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 00000000..86663826 --- /dev/null +++ b/include/pear/Mail/RFC822.php @@ -0,0 +1,940 @@ + | +// | Chuck Hagenbuch | +// +-----------------------------------------------------------------------+ + +/** + * 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" (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes + * @author Chuck Hagenbuch + * @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 + // ... 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* + * + * 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('/(?@. 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 00000000..b48d2697 --- /dev/null +++ b/include/pear/Mail/mail.php @@ -0,0 +1,143 @@ + | +// +----------------------------------------------------------------------+ +// +// $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 00000000..2286920d --- /dev/null +++ b/include/pear/Mail/mime.php @@ -0,0 +1,1095 @@ + + * Copyright (c) 2003-2006, PEAR + * 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 + * @author Tomas V.V. Cox + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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 which was based also + * in the mime_mail.class by Tobias Ratschiller + * and Sascha Schumann + */ + + +/** + * 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 + * @author Tomas V.V. Cox + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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 + //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 + //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 00000000..b7984d68 --- /dev/null +++ b/include/pear/Mail/mimeDecode.php @@ -0,0 +1,860 @@ + + * Copyright (c) 2003-2006, PEAR + * 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 + * @author George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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 + * @author George Schlossnagle + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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_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 + */ + 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 = '' . $crlf . + '' . $crlf . + '' . $crlf . + Mail_mimeDecode::_getXML($input) . + ''; + + 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) . '' . $crlf . + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . + str_repeat($htab, $indent) . '' . $crlf; + } + } elseif (isset($input->body)) { + $output .= $crlf . str_repeat($htab, $indent) . '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 . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramname) . '' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '' . htmlspecialchars($paramvalue) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . $crlf; + } + + $params = implode('', $params); + } else { + $params = ''; + } + + $return = str_repeat($htab, $indent) . '
' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_name) . '' . $crlf . + str_repeat($htab, $indent) . $htab . '' . htmlspecialchars($new_hdr_value['value']) . '' . $crlf . + $params . + str_repeat($htab, $indent) . '
' . $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 00000000..7ad54e5e --- /dev/null +++ b/include/pear/Mail/mimePart.php @@ -0,0 +1,439 @@ + + * Copyright (c) 2003-2006, PEAR + * 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 + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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 + * @author Cipriano Groenendal + * @author Sean Coates + * @copyright 2003-2006 PEAR + * @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 00000000..1dfdadb8 --- /dev/null +++ b/include/pear/Mail/mock.php @@ -0,0 +1,119 @@ + | +// +----------------------------------------------------------------------+ +// +// $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 00000000..982bfa45 --- /dev/null +++ b/include/pear/Mail/null.php @@ -0,0 +1,60 @@ + | +// +----------------------------------------------------------------------+ +// +// $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 00000000..183890a8 --- /dev/null +++ b/include/pear/Mail/sendmail.php @@ -0,0 +1,171 @@ + | +// +----------------------------------------------------------------------+ + +/** + * 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 00000000..850695d3 --- /dev/null +++ b/include/pear/Mail/smtp.php @@ -0,0 +1,421 @@ + | +// | Jon Parise | +// +----------------------------------------------------------------------+ + +/** 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 00000000..bf0a5a1a --- /dev/null +++ b/include/pear/Net/SMTP.php @@ -0,0 +1,1080 @@ + | +// | Jon Parise | +// | Damian Alejandro Fernandez Sosa | +// +----------------------------------------------------------------------+ +// +// $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 + * @author Jon Parise + * @author Damian Alejandro Fernandez Sosa + * + * @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('/(?_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 00000000..cc495bf9 --- /dev/null +++ b/include/pear/Net/Socket.php @@ -0,0 +1,593 @@ + | +// | Chuck Hagenbuch | +// +----------------------------------------------------------------------+ +// +// $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 + * @author Chuck Hagenbuch + */ +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: + * + *

+ * timed_out (bool) - The socket timed out waiting for data
+ * blocked (bool) - The socket was blocked
+ * eof (bool) - Indicates EOF event
+ * unread_bytes (int) - Number of bytes left in the socket buffer
+ *

+ * + * @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 00000000..4de89ee3 --- /dev/null +++ b/include/pear/PEAR.php @@ -0,0 +1,1129 @@ + + * @author Stig Bakken + * @author Tomas V.V.Cox + * @author Greg Beaver + * @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 + * @author Tomas V.V. Cox + * @author Greg Beaver + * @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 + * @author Tomas V.V. Cox + * @author Gregory Beaver + * @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 00000000..da77106f --- /dev/null +++ b/include/pear/PEAR/FixPHP5PEARWarnings.php @@ -0,0 +1,7 @@ + diff --git a/include/pear/PEAR5.php b/include/pear/PEAR5.php new file mode 100644 index 00000000..5cee0903 --- /dev/null +++ b/include/pear/PEAR5.php @@ -0,0 +1,33 @@ +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; +?> +
API Keys
+
+
+ + + + + + 0): //Show options.. + ?> + + + + + +
+ + + + + + + + + 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; + } + ?> + + + + + + + + + + +
 API KeyActive  IP Address   + Created
+ + onClick="highLight(this.value,this.checked);"> +  Yes':'No'; ?>  
Query returned 0 results  Index list
+
+ + + +    + + +    + +
+
+
Add New IP
+
+
+ Add a new IP address.   +
+ + + New IP: + +    +    +
+
+
+
API Passphrase
+
+
+ Passphrase must be at least 3 words. Required to generate the api keys.
+
+ + + Phrase: + +    +    +
+

+
Please note that changing the passprase does NOT invalidate existing keys. To regerate a key you need to delete and readd it.
+
diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php new file mode 100644 index 00000000..5a82ee39 --- /dev/null +++ b/include/staff/apikey.inc.php @@ -0,0 +1,104 @@ +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); +?> +
+ + + +

API Key

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ API Key is autogenerated and unique per IP address. +
+ Status: + + >Active + >Disabled +   +
+ IP Address: + + getIPAddr(); ?> +
+ API Key: + getKey(); ?>   (Delete and re-add to change the key)
+ IP Address: + + +   +
+ Enabled Services:: Check applicable API services. +
+ Email piping: + + + Enable remote email piping. +
+ Admin Notes: Internal notes.  +
+ +
+

+ + + +

+
diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php new file mode 100644 index 00000000..26468089 --- /dev/null +++ b/include/staff/apikeys.inc.php @@ -0,0 +1,120 @@ +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!'; + +?> + +
+

API Keys

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 href="apikeys.php?&sort=date">Date Added href="apikeys.php?&sort=key">API Key href="apikeys.php?&sort=status">Status href="apikeys.php?&sort=ip">IP Addr.href="apikeys.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">   Disabled'; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' 
'; +?> +

+ + + +

+ + + diff --git a/include/staff/attachment.inc.php b/include/staff/attachment.inc.php new file mode 100644 index 00000000..17819867 --- /dev/null +++ b/include/staff/attachment.inc.php @@ -0,0 +1,82 @@ +isadmin()) die('Access Denied'); +//Get the config info. +$config=($errors && $_POST)?Format::input($_POST):$cfg->getConfig(); +?> + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Attachments Settings
+ Before enabling attachments make sure you understand the security settings and issues related to file uploads.
Allow Attachments: + >Allow Attachments +   (Global Setting) +    +
Emailed Attachments: + > Accept emailed files +    +
Online Attachments: + > + Allow online attachments upload
     + > + Authenticated users Only. (User must be logged in to upload files ) +   +
Staff Response Files: + >Email attachments to the user +
Maximum File Size: + bytes +   +
Attachment Folder: + Web user (e.g apache) must have write access to the folder.   
+ + + + +

Accepted File Types:
+ Enter file extensions allowed separated by a comma. e.g .doc, .pdf,
+ To accept all files enter wildcard .*  i.e dotStar (NOT recommended). + +
+
+ + +
diff --git a/include/staff/banlist.inc.php b/include/staff/banlist.inc.php new file mode 100644 index 00000000..59789642 --- /dev/null +++ b/include/staff/banlist.inc.php @@ -0,0 +1,145 @@ +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; +?> +

Banned Email Addresses

+
+
+ +
+ Query: +    + +
+
+
+ +
+showing(); +else + $showing='No banned emails matching the query found!'; + +if($search) + $showing='Search Results: '.$showing; + +?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="staff.php?&sort=email">Email Address href="staff.php?&sort=status">Ban Status href="staff.php?&sort=created">Date Added href="staff.php?&sort=updated">Last Updated
+ + onClick="highLight(this.value,this.checked);"> +    Disabled'; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' 
'; +?> +

+ +    + +    + +

+ + + diff --git a/include/staff/banrule.inc.php b/include/staff/banrule.inc.php new file mode 100644 index 00000000..bd28d19d --- /dev/null +++ b/include/staff/banrule.inc.php @@ -0,0 +1,79 @@ +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); +?> +
+ + + +

Manage Email Ban List

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Valid email address required. +
+ Filter Name: + getName(); ?>
+ Ban Status: + + >Active + >Disabled +   +
+ Email Address: + + +   +
+ Internal notes: Admin notes  +
+ +
+

+ + + +

+
diff --git a/include/staff/cannedreplies.inc.php b/include/staff/cannedreplies.inc.php new file mode 100644 index 00000000..db44a79f --- /dev/null +++ b/include/staff/cannedreplies.inc.php @@ -0,0 +1,129 @@ +'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!'; + +?> +
+

Canned Replies

+
+
+ Add New Reply
+
+
+ + + + + + + + + + + + + + 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']?' ':''; + ?> + + + + + + + + + + + + + +
  href="canned.php?&sort=title">Title href="canned.php?&sort=status">Status href="canned.php?&sort=dept">Departmenthref="canned.php?&sort=updated">Last Updated
+ + onClick="highLight(this.value,this.checked);"> +   + Disabled'; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ + + +

+ +
diff --git a/include/staff/cannedreply.inc.php b/include/staff/cannedreply.inc.php new file mode 100644 index 00000000..fa8e4615 --- /dev/null +++ b/include/staff/cannedreply.inc.php @@ -0,0 +1,115 @@ +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); + +?> +
+ + + +

Canned Reply

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Canned reply settings +
Status: + >Active + >Disabled +   +
Department: + +   +
+ Canned Reply: Make the title short and clear.  +
+
Title
+ +

Canned Response +    (Supported Variables)
+ +

Canned Attachments (optional)  
+ getAttachments())) { + echo '
Uncheck to delete the attachment on submit
'; + foreach($files as $file) { + $hash=$file['hash'].md5($file['id'].session_id()); + echo sprintf(' ', + $file['id'], $file['id'], $hash, $file['name']); + } + echo '

'; + + } + //Hardcoded limit... TODO: add a setting on admin panel - what happens on tickets page?? + if(count($files)<10) { + ?> +
+ +
+ +
You can upload up to 10 attachments per canned response.
+
+ Internal Notes: Notes about the canned reply.  +
+ +
+

+ + + +

+
diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php new file mode 100644 index 00000000..bba67989 --- /dev/null +++ b/include/staff/categories.inc.php @@ -0,0 +1,124 @@ +'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!'; + +?> +
+

FAQ Categories

+
+
+ Add New Category
+
+
+ + + + + + + + + + + + + + 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('%d',$row['category_id'],$row['faqs']); + + ?> + + + + + + + + + + + + + +
  href="categories.php?&sort=name">Name href="categories.php?&sort=type">Type href="categories.php?&sort=faqs">FAQshref="categories.php?&sort=updated">Last Updated
+ + onClick="highLight(this.value,this.checked);">  Public':'Internal'; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ + + +

+ +
diff --git a/include/staff/category.inc.php b/include/staff/category.inc.php new file mode 100644 index 00000000..8272d7da --- /dev/null +++ b/include/staff/category.inc.php @@ -0,0 +1,79 @@ +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); + +?> +
+ + + +

FAQ Category

+ + + + + + + + + + + + + + + + + + + + + + + + +
+

+
+ Category information: Public categories are published if it has published FAQ articles. +
Category Type: + >Public (publish) +      + >Private (internal) +   +
+
Category NameShort descriptive name.
+ +   +
+
+ Category DescriptionSummary of the category. +   +
+ +
+ Internal Notes  +
+ +
+

+ + + +

+
diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php new file mode 100644 index 00000000..fa32ec27 --- /dev/null +++ b/include/staff/department.inc.php @@ -0,0 +1,210 @@ +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); +?> +
+ + + +

Department

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + getNumUsers()){ ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Department Information +
+ Name: + + +   +
+ Type: + + >Public + >Private (Internal) +   +
+ Email: + + +   +
+ Template: + + +   +
+ SLA: + + +   +
+ Manager: + + +    +
+ Auto Response Settings: Overwrite global auto-response settings for tickets routed to the Dept. +
+ New Ticket: + + > + + Disable new ticket auto-response for this Dept. +
+ New Message: + + > + Disable new message auto-response for this Dept. +
+ Auto Response Email: + + +    +
+ Department Signature: Optional signature used on outgoing emails.    +
+ +
Signature is made available as a choice, on ticket reply, for public departments. +
+

+ + + +

+
diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php new file mode 100644 index 00000000..805b7ec2 --- /dev/null +++ b/include/staff/departments.inc.php @@ -0,0 +1,130 @@ +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!'; + +?> +
+

Departments

+
+
+ Add New Department
+
+
+ + + + + + + + + + + + + + + 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'])?' (Default)':''; + ?> + + + + + + + + + + + + + + +
  href="departments.php?&sort=name">Name href="departments.php?&sort=type">Typehref="departments.php?&sort=users">Users href="departments.php?&sort=email">Email Address href="departments.php?&sort=manager">Dept. Manager
+ + onClick="highLight(this.value,this.checked);">  Private'; ?>   + + 0) { ?> + + 0 + + +  
+ + Select:  + All   + None   + Toggle   + +
+ +

+ + + +

+ + +
+ diff --git a/include/staff/directory.inc.php b/include/staff/directory.inc.php new file mode 100644 index 00000000..b687d811 --- /dev/null +++ b/include/staff/directory.inc.php @@ -0,0 +1,137 @@ +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; +?> +

Staff Members

+
+
+ + +    + +
+
+
+showing(); +else + $showing='No staff members found!'; +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
href="directory.php?&sort=name">Namehref="directory.php?&sort=dept">Departmenthref="directory.php?&sort=email">Email Address href="directory.php?&sort=phone">Phone Number href="directory.php?&sort=ext">Phone Ext href="directory.php?&sort=mobile">Mobile Number
      
+  Page:'.$pageNav->getPageLinks().' '; + ?> + +
+ diff --git a/include/staff/editticket.inc.php b/include/staff/editticket.inc.php new file mode 100644 index 00000000..20accf0e --- /dev/null +++ b/include/staff/editticket.inc.php @@ -0,0 +1,142 @@ +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! */ +} + +?> +
+ +

+ +

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Update Ticket #getExtId(); ?>  (View Ticket)
Email Address: + +  *  +
Full Name: + +  *  +
Subject: + +   +
Telephone: +  Ext  +   
 
Due Date: + Time is based on your time zone (GM getTZoffset(); ?>)  
+ + +    + +    +
Priority: + +
Help Topic: + +  (optional)  +
Internal Note: + Reasons for the edit.
+
 
+ + + +
+ diff --git a/include/staff/email.inc.php b/include/staff/email.inc.php new file mode 100644 index 00000000..4f2d695b --- /dev/null +++ b/include/staff/email.inc.php @@ -0,0 +1,257 @@ +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); +?> +

Email Address

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Email Information: Login details are optional BUT required when IMAP/POP or SMTP are enabled. +
+ Email Address + + +   +
+ Email Name + + +    +
+ Login Username + + +     +
+ Login Password + + +     +
+
+ Mail Account: Optional setting for fetching incoming emails. Mail fetching must be enabled with autocron active or external cron setup.    +
Status + +    + +    +
Host +    +
Port +    +
Protocol + +   +
Encryption + + +   +
Fetch Frequency + Delay intervals in minutes +    +
Emails Per Fetch + Maximum emails to process per fetch. +    +
+ New Ticket Priority: + + +   +
+ New Ticket Dept. + + +   +
+ Auto-response + + > + Disable new ticket auto-response for this email. Overwrite global and dept. settings. +
Fetched Emails + > + Move to: folder. +    + > + Delete fetched emails + > + Do nothing (Not recommended) +
Moving fetched emails to a backup folder is highly recommended.   +
+ SMTP Settings: When enabled the email account will use SMTP server instead of internal PHP mail() function for outgoing emails.    +
Status + + +    +
SMTP Host +    +
SMTP Port +    +
Authentication Required? + + + +   +
Allow Header Spoofing? + > + Allow email header spoofing (only applies to emails being sent through this account) +
+ Internal Notes: Admin's notes.    +
+ +
+

+ + + +

+
diff --git a/include/staff/emails.inc.php b/include/staff/emails.inc.php new file mode 100644 index 00000000..44e277b4 --- /dev/null +++ b/include/staff/emails.inc.php @@ -0,0 +1,130 @@ +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!'; + +?> +
+

Email Addresses

+
+
+ Add New Email
+
+
+ + + + + + + + + + + + + + + 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'].'>'; + ?> + + + + + + + + + + + + + + +
  href="emails.php?&sort=email">Email href="emails.php?&sort=priority">Priority href="emails.php?&sort=dept">Departmenthref="emails.php?&sort=created">Createdhref="emails.php?&sort=updated">Last Updated
+ +   + + + onClick="highLight(this.value,this.checked);"> + +    
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ +

+ +
+ diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php new file mode 100644 index 00000000..cdbb266d --- /dev/null +++ b/include/staff/faq-view.inc.php @@ -0,0 +1,66 @@ +getCategory(); + +?> +

Frequently Asked Questions

+ +
+getQuestion() ?>  isPublished()?'(Published)':''; ?> +
+
+canManageFAQ()) { + echo sprintf('Edit FAQ', + $faq->getId()); +} +?> +  +
+
+

+getAnswer()); ?> +

+

+

Attachments: getAttachmentsLinks(); ?>
+
Help Topics: + getHelpTopics())?implode(', ',$topics):' '; ?> +
+

+
 Last updated getUpdateDate()); ?>
+
+canManageFAQ()) { + //TODO: add js confirmation.... + ?> +
+
+ + +
+ Options: + +    +
+
+
+ diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php new file mode 100644 index 00000000..8882da2f --- /dev/null +++ b/include/staff/faq.inc.php @@ -0,0 +1,150 @@ +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); +?> +
+ + + +

FAQ

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+
+ FAQ Information +
+
Question 
+ +
+
Category ListingFAQ category the question belongs to.
+ + +
+
Listing Type:  + Published questions are listed on public knowledgebase if the parent category is public.
+ >Public (publish) +      + >Internal (private) +   +
+
+ Answer 
+ +
+
Attachments (optional)  
+ getAttachments())) { + echo '
Uncheck to delete the attachment on submit
'; + foreach($files as $file) { + $hash=$file['hash'].md5($file['id'].session_id().$file['hash']); + echo sprintf(' ', + $file['id'], $file['id'], $hash, $file['name']); + } + echo '

'; + } + //TODO: add a setting on admin panel + if(count($files)<5) { + ?> +
+ +
+ +
You can upload up to 5 attachments.
+
+ Help Topics: Check all help topics related to this FAQ. +
+ %s
', + $topicId, + (($info['topics'] && in_array($topicId,$info['topics']))?'checked="checked"':''), + $topic); + } + ?> +
+ Internal Notes:   +
+ +
+

+ + + +

+
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php new file mode 100644 index 00000000..91f74501 --- /dev/null +++ b/include/staff/filter.inc.php @@ -0,0 +1,307 @@ +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); +?> +
+ + + +

Incoming Email Filter

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getNumRules():0)+2; //2 extra rules of unlimited. + for($i=1; $i<=$n; $i++){ ?> + + + + =25) //Hardcoded limit of 25 rules...also see class.filter.php + break; + } ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Filters are executed based on execution order. +
+ Filter Name: + + +   +
+ Execution Order: + + + (1...99 ) +   +     + > + Stop processing further on match! +
+ Filter Status: + + >Active + >Disabled +   +
+ To Email Address: + + +
(Highly recommended if the filter is specific to one incoming email address) +
+ Filter Rules: Rules are applied based on the criteria.  + +
+ Rules Matching Criteria: +        + >Match All +     + >Match Any +   + (case-insensitive comparison) + +
+
+ + + "> +    +
+ + + +
+
+ Filter Actions: Can be overwriten by other filters depending on processing order.  +
+ Ban Email: + + > + Reject email (All other actions, rules and filters are ignored) +
+ Reply-To Email: + + > + Use Reply-To Email (if available) +
+ Ticket auto-response: + + > + Disable auto-response. (Overwrites Dept. settings) +
+ Department: + + +   +
+ Priority: + + +   + (Overwrites department's priority) +
+ SLA Plan: + + +    + (Overwrites department's SLA) +
+ Auto-assign To: + + +    +
+ Admin Notes: Internal notes.  +
+ +
+

+ + + +

+
diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php new file mode 100644 index 00000000..4b9d6f31 --- /dev/null +++ b/include/staff/filters.inc.php @@ -0,0 +1,126 @@ +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!'; + +?> + +
+

Email Filters

+
+
+ Add New Filter
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="filters.php?&sort=name">Name href="filters.php?&sort=status">Status href="filters.php?&sort=order">Order href="filters.php?&sort=rules">Ruleshref="filters.php?&sort=created">Date Addedhref="filters.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">  Disabled'; ?>    
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ + + +

+ +
+ diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php new file mode 100644 index 00000000..2591a55a --- /dev/null +++ b/include/staff/footer.inc.php @@ -0,0 +1,16 @@ + + +isStaff()) { ?> +
+ + + +
+ + + + diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php new file mode 100644 index 00000000..87921a63 --- /dev/null +++ b/include/staff/group.inc.php @@ -0,0 +1,165 @@ +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); +?> +
+ + + +

User Group

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ',$id,$ck,$name); + } + } + ?> + + + + + + + +
+

+ Group Information: Disabled group will limit staff members access. Admins are exempted. +
+ Name: + + +   +
+ Status: + + >Active + >Disabled +   +
+ Group Permissions: Applies to all group members  +
Can Create Tickets + />Yes +    + />No +   Ability to open tickets on behalf of clients. +
Can Edit Tickets + />Yes +    + />No +   Ability to edit tickets. +
Can Close Tickets + />Yes +    + />No +   Ability to close tickets. Staff can still post a response. +
Can Assign Tickets + />Yes +    + />No +   Ability to assign tickets to staff members. +
Can Transfer Tickets + />Yes +    + />No +   Ability to transfer tickets between departments. +
Can Delete Tickets + />Yes +    + />No +   Deleted tickets can't be recovered! +
Can Ban Emails + />Yes +    + />No +   Ability to add/remove emails from banlist via ticket interface. +
Can Manage Premade + />Yes +    + />No +   Ability to add/update/disable/delete canned responses and attachments. +
Can Manage FAQ + />Yes +    + />No +   Ability to add/update/disable/delete knowledgebase categories and FAQs. +
+ Department Access: Check all departments the group members are allowed to access.   Select All  Select None +
  %s
+ Admin Notes: Internal notes viewable by all admins.  +
+ +
+

+ + + +

+
diff --git a/include/staff/groups.inc.php b/include/staff/groups.inc.php new file mode 100644 index 00000000..15b9b661 --- /dev/null +++ b/include/staff/groups.inc.php @@ -0,0 +1,121 @@ +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!'; + +?> +
+

User Groups

+
+
+ Add New Group
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="groups.php?&sort=name">Group Name href="groups.php?&sort=status">Group Statushref="groups.php?&sort=users">Members href="groups.php?&sort=created">Created On href="groups.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">   Disabled'; ?>   + 0) { ?> + + 0 + +   +   
+ + Select:  + All   + None   + Toggle   + +
+ +

+ + + +

+ + +
+ diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php new file mode 100644 index 00000000..590673c1 --- /dev/null +++ b/include/staff/header.inc.php @@ -0,0 +1,79 @@ + + + + + osTicket Staff Control Panel + + + + + + + + + +
+ + + +
+ +
+ +
+ +
+ diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php new file mode 100644 index 00000000..ad9f808b --- /dev/null +++ b/include/staff/helptopic.inc.php @@ -0,0 +1,198 @@ +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); +?> +
+ + + +

Help Topic

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Help Topic Information +
+ Topic: + + +   +
+ Status: + + >Active + >Disabled +   +
+ Type: + + >Public + >Private (Internal) +   +
+ Priority: + + +   +
+ Department: + + +   +
+ SLA Plan: + + +    + (Overwrites department's SLA) +
+ Auto-assign To: + + +    +
+ Ticket auto-response: + + > + Disable new ticket auto-response for this topic (Overwrites Dept. settings). +
+ Admin Notes: Internal notes about the help topic.  +
+ +
+

+ + + +

+
diff --git a/include/staff/helptopics.inc.php b/include/staff/helptopics.inc.php new file mode 100644 index 00000000..51a76668 --- /dev/null +++ b/include/staff/helptopics.inc.php @@ -0,0 +1,129 @@ +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!'; + +?> +
+

Help Topics

+
+ +
+
+ + + + + + + + + + + + + + + + getDefaultDeptId(); + while ($row = db_fetch_array($res)) { + $sel=false; + if($ids && in_array($row['topic_id'],$ids)){ + $class="$class highlight"; + $sel=true; + } + ?> + + + + + + + + + + + + + + + +
  href="helptopics.php?&sort=name">Help Topic href="helptopics.php?&sort=status">Status href="helptopics.php?&sort=type">Type href="helptopics.php?&sort=priority">Priority href="helptopics.php?&sort=dept">Departmenthref="helptopics.php?&sort=updated">Last Updated
+ + onClick="highLight(this.value,this.checked);">  Disabled'; ?>Private'; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' 
'; +?> +

+ + + +

+ + + + diff --git a/include/staff/index.php b/include/staff/index.php new file mode 100644 index 00000000..f9518a67 --- /dev/null +++ b/include/staff/index.php @@ -0,0 +1,3 @@ + diff --git a/include/staff/kb-categories.inc.php b/include/staff/kb-categories.inc.php new file mode 100644 index 00000000..0ab8b03f --- /dev/null +++ b/include/staff/kb-categories.inc.php @@ -0,0 +1,114 @@ + +

Frequently Asked Questions

+
+ + + + + + + + + +
+ + + + +
+ +
+
+
+
+Search Results
"; + if(($res=db_query($sql)) && db_num_rows($res)) { + echo '
+
    '; + while($row=db_fetch_array($res)) { + echo sprintf(' +
  1. %s - %s
  2. ', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo '
+
'; + } else { + echo 'The search did not match any FAQs.'; + } +} 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 '
Click on the category to browse FAQs.
+
    '; + while($row=db_fetch_array($res)) { + + echo sprintf(' +
  • +

    %s (%d) - %s

    + %s +
  • ',$row['category_id'],$row['name'],$row['faqs'], + ($row['ispublic']?'Public':'Internal'), + Format::safe_html($row['description'])); + } + echo '
'; + } else { + echo 'NO FAQs found'; + } +} +?> +
diff --git a/include/staff/kb-category.inc.php b/include/staff/kb-category.inc.php new file mode 100644 index 00000000..65c428a2 --- /dev/null +++ b/include/staff/kb-category.inc.php @@ -0,0 +1,50 @@ + +
+

Frequently Asked Questions

+
+
 
+
+
+
+ getName() ?> + (isPublic()?'Public':'Internal'; ?>) +
+
 Last updated getUpdateDate()); ?>
+
+

+getDescription()); ?> +

+canManageFAQ()) { + echo sprintf('Edit Category + | Delete Category + | Add New FAQ', + $category->getId(), + $category->getId()); +} +?> +
+getId()) + .' GROUP BY faq.faq_id'; +if(($res=db_query($sql)) && db_num_rows($res)) { + echo '
+
    '; + while($row=db_fetch_array($res)) { + echo sprintf(' +
  1. %s - %s
  2. ', + $row['faq_id'],$row['question'],$row['ispublished']?'Published':'Internal'); + } + echo '
+
'; +}else { + echo 'Category does not have FAQs'; +} +?> diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php new file mode 100644 index 00000000..d2b94d55 --- /dev/null +++ b/include/staff/login.tpl.php @@ -0,0 +1,27 @@ + + + + +osTicket:: SCP Login + + + + + + +
+

osTicket Staff Control Panel

+

+
+
+ + + + + +
Username:
Password:
   
+
+
+
Copyright © osTicket.com
+ + diff --git a/include/staff/newticket.inc.php b/include/staff/newticket.inc.php new file mode 100644 index 00000000..42afb7c3 --- /dev/null +++ b/include/staff/newticket.inc.php @@ -0,0 +1,224 @@ +isStaff()) die('Access Denied'); +$info=($_POST && $errors)?Format::input($_POST):array(); //on error...use the post data +?> +
+ +

+ +

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + canUploadFiles()) { + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Please fill in the form below to open a new ticket.
Email Address: + +  *  + notifyONNewStaffTicket()) { ?> +     + >Send alert to user. + +
Full Name: + +  *  +
Telephone: +  Ext  +  
  +
Ticket Source: + +  *  +
Department: + +  *  +
Subject: + +   +
Issue Summary: + Visible to client/customer.
+ + Premade:  +     + +
Attachment: +   +
Internal Note: + Optional Internal note(s). 
+
Due Date: + Time is based on your time zone (GM getTZoffset(); ?>)  
+ + +    + +    +
Priority: + +
Help Topic: + +    +
Assign To: +   +     + >Send alert to assigned staff. +
Signature: appendMySignature(); + $info['signature']=!$info['signature']?'none':$info['signature']; //change 'none' to 'mine' to default to staff signature. + ?> +
+ + + + + +
+
  +
+ + + +
+ + diff --git a/include/staff/preference.inc.php b/include/staff/preference.inc.php new file mode 100644 index 00000000..d956d011 --- /dev/null +++ b/include/staff/preference.inc.php @@ -0,0 +1,498 @@ +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']='
*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())); +?> +
System Preferences and Settings  (v)
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
General Settings
Offline mode will disable client interface and only allow super admins to login to Staff Control Panel
Helpdesk Status + />Online (Active) + />Offline (Disabled) +    +
Helpdesk URL: + +  
Helpdesk Name/Title:
Default Email Templates: +   +
Default Department: +   +
Default Page Size: + +
System Log Level: + +  Purge logs after + +
Staff Excessive Logins: + attempt(s) allowed +  before a + min. timeout (penalty in minutes) +
Staff Session Timeout: + + (Staff's max Idle time in minutes. Enter 0 to disable timeout) +
Bind Staff Session to IP: + > + Bind staff's session to login IP. +
Client Excessive Logins: + attempt(s) allowed +  before a + min. timeout (penalty in minutes) +
Client Session Timeout: + + (Client's max Idle time in minutes. Enter 0 to disable timeout) +
Clickable URLs: + > + Make URLs clickable +
Enable Auto Cron: + > + Enable cron call on staff's activity +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Date & Time
Please refer to PHP Manual for supported parameters.
Time Format: + +   +
Date Format: +   + +
Date & Time Format: +   + +
Day, Date & Time Format: +   + +
Default Timezone: + +
Daylight Saving: + >Observe daylight savings +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ticket Options & Settings
If enabled ticket lock get auto-renewed on form activity.
Ticket IDs: + /> Sequential + />Random (recommended) +
Ticket Priority: +  Default priority
+ > + Allow user to overwrite/set priority (new web tickets)
+ > + Use email priority when available (new emailed tickets) + +
Maximum Open Tickets: + + per email. (Helps with spam and flood control. Enter 0 for unlimited) +
Auto-Lock Time: + + + + (Minutes to lock a ticket on activity. Enter 0 to disable locking) +
Ticket Grace Period: + + (Hours before ticket is marked overdue. Enter 0 to disable aging.) +
Reopened Tickets: + > + Auto-assign reopened tickets to last respondent 'available'. ( 3 months limit) +
Assigned Tickets: + > + Show assigned tickets on open queue. +
Answered Tickets: + > + Show answered tickets on open queue. +
Ticket Activity Log: + > + Log ticket's activity as internal notes. +
Staff Identity: + > + Hide staff's name on responses. +
Human Verification: + +   + + > + Enable captcha on new web tickets.  
+
+ + + + + + + + + + + + + + + + + + +
Email Settings
Note that global settings can be disabled at dept/email level.

Incoming Emails:
For mail fetcher (POP/IMAP) to work you must set a cron job or simply enable auto-cron
+ > Enable POP/IMAP email fetch +   (Global setting which can be disabled at email level)
+ > Enable email piping +  (You pipe we accept policy)
+ > + Strip quoted reply (depends on the tag below)
+ Reply Separator Tag +    +

Outgoing Emails:
+ Default Email: Only applies to outgoing emails with no SMTP settings.
+    
+ + > + Allow spoofing (No Overwrite).  
+
+
Default System Email: + +  
Default Alert Email: + +   +
Used to send out alerts and notices to staff. +
System Admin Email Address: + +  
+ + + + + + + + + + + + + + + + +
Autoresponders  (Global Setting)
This is global setting which can be disabled at department level.
New Ticket:Autoresponse includes the ticket ID required to check status of the ticket
+ />Enable + />Disable +
New Ticket by Staff:Notice sent when staff creates a ticket on behalf of the user (Staff can disable)
+ />Enable + />Disable +
New Message:Message appended to an existing ticket confirmation
+ />Enable + />Disable +
Overlimit notice:Ticket denied notice sent only once on limit violation to the user.
+ />Enable + />Disable +
Note: Admin gets alerts on ALL denials by default.
+
+ + + + + + + + + + + + + + + + + + + + +
 Alerts & Notices
+ Notices sent to user use 'No Reply Email' whereas alerts to staff use 'Alert Email' set above as FROM address respectively.
New Ticket Alert: + />Enable + />Disable +
Select recipients  
+ > Admin Email + > Department Manager + > Department Members (spammy) +
New Message Alert: + />Enable + />Disable +
Select recipients  
+ > Last Respondent + > Assigned Staff + > Department Manager (spammy) +
New Internal Note Alert: + />Enable + />Disable +
Select recipients  
+ > Last Respondent + > Assigned Staff + > Department Manager (spammy) +
Overdue Ticket Alert: + />Enable + />Disable +
Admin Email gets an alert by default. Select additional recipients below  
+ > Assigned Staff + > Department Manager + > Department Members (spammy) +
System Errors:Enabled errors are sent to admin email set above
+ disabled>System Errors + >SQL errors + >Excessive Login attempts +
+
+ + +
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php new file mode 100644 index 00000000..e2b883e1 --- /dev/null +++ b/include/staff/profile.inc.php @@ -0,0 +1,221 @@ +getInfo(); +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +$info['id']=$staff->getId(); +?> +
+ + +

My Account Profile

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + isAdmin() || $staff->isManager()){ ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Account Information

+ Contact information. +
+ Username: + getUserName(); ?>
+ First Name: + + +   +
+ Last Name: + + +   +
+ Email Address: + + +   +
+ Phone Number: + + +    + Ext +    +
+ Mobile Number: + + +    +
+ Preferences: Profile preferences and settings. +
+ Time Zone: + + +   +
+ Daylight Saving: + + > + Observe daylight saving + (Current Time: getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['daylight_saving']); ?>) +
Maximum Page size: + per page. +
Auto Refresh Rate: + + (Tickets page refresh rate in minutes.) +
Default Signature: + + (You can change selection on ticket page) +    +
Show Assigned Tickets: + > + Show assigned tickets on open queue. +
+ Password: To reset your password, provide your current password and a new password below.   +
+ Current Password: + + +    +
+ New Password: + + +    +
+ Confirm New Password: + + +    +
+ Signature: Optional signature used on outgoing emails. +    +
+ +
Signature is made available as a choice, on ticket reply. +
+

+ + + +

+
diff --git a/include/staff/settings-alerts.inc.php b/include/staff/settings-alerts.inc.php new file mode 100644 index 00000000..a71b9e8a --- /dev/null +++ b/include/staff/settings-alerts.inc.php @@ -0,0 +1,102 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Alerts and Notices Setting

+ Alerts sent to staff on ticket "events". Staff assignment takes precedence over team assignment. +
New Ticket Alert: + />Enable + />Disable +   Alert sent out on new tickets  
+ Recipients:  + > Admin Email + > Department Manager + > Department Members (spammy) +
New Message Alert: + />Enable + />Disable +   Alert sent out when a new message is appended to an existing ticket  
+ Recipients:  + > Last Respondent + > Assigned Staff + > Department Manager (spammy) +
New Internal Note Alert: + />Enable + />Disable +   Alert sent out when a new internal note is posted   
+ Recipients:  + > Last Respondent + > Assigned Staff + > Department Manager (spammy) +
Ticket Assignment Alert: + Enable + Disable +   Alert sent out to staff on ticket assignment   
+ Recipients:  + > Assigned Staff + >Team Lead (Team assignment) + > + Team Members (spammy) +
Ticket Transfer Alert: + />Enable + />Disable +   Alert sent out to staff on ticket transfer   +
+ Recipients:  + > Assigned Staff/Team + > Department Manager + > Department Members + (spammy) +
Overdue Ticket Alert: + />Enable + />Disable +   Alert sent out when a ticket becomes overdue - admin email gets an alert by default.   
+ Recipients:  + > Assigned Staff/Team + > Department Manager + > Department Members (spammy) +
System Alerts:Enabled: Errors are sent to system admin email (getAdminEmail(); ?>)
+ System Errors + >SQL errors + >Excessive Login attempts +
+

+ + +

+
diff --git a/include/staff/settings-attachments.inc.php b/include/staff/settings-attachments.inc.php new file mode 100644 index 00000000..572c5da5 --- /dev/null +++ b/include/staff/settings-attachments.inc.php @@ -0,0 +1,107 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Attachments Settings

+ Before enabling attachments make sure you understand PHP file upload settings and security issues related to file upload. +
Allow Attachments: + >Allow Attachments +   (Global Setting) +    +
Emailed Attachments: + > Accept emailed files +    +
Online Attachments: + > + Allow web upload      + > + Limit to authenticated users only. (User must be logged in to upload files) +   +
Max. User File Uploads: + + (Number of files the user is allowed to upload simultaneously) +    +
Max. Staff File Uploads: + + (Number of files the staff is allowed to upload simultaneously) +    +
Maximum File Size: + in bytes. + (Max ) +   +
Ticket Response Files: + >Email attachments to the user +
+ Accepted File Types: Limit the type of files users are allowed to upload. +   +
+ Enter allowed file extensions separated by a comma. e.g .doc, .pdf. To accept all files enter wildcard .* i.e dotStar (NOT Recommended).
+ +
+

+ + +

+
diff --git a/include/staff/settings-autoresponders.inc.php b/include/staff/settings-autoresponders.inc.php new file mode 100644 index 00000000..70bddc82 --- /dev/null +++ b/include/staff/settings-autoresponders.inc.php @@ -0,0 +1,56 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Autoresponder Setting

+ Global setting - can be disabled at department or email level. +
New Ticket: + />Enable + />Disable +     + (Autoresponse includes the ticket ID required to check status of the ticket) +
New Ticket by staff: + />Enable + />Disable +     + (Notice sent when staff creates a ticket on behalf of the user (Staff can overwrite)) +
New Message: + />Enable + />Disable +     + (Confirmation notice sent when a new message is appended to an existing ticket) +
Overlimit notice: + />Enable + />Disable +     + (Ticket denied notice sent to user on limit violation. Admin gets alerts on ALL denials by default) +
+

+ + +

+
diff --git a/include/staff/settings-dates.inc.php b/include/staff/settings-dates.inc.php new file mode 100644 index 00000000..0434e94c --- /dev/null +++ b/include/staff/settings-dates.inc.php @@ -0,0 +1,69 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Date and Time Options

+ Please refer to PHP Manual for supported parameters. +
Time Format: + +   +
Date Format: +   + +
Date & Time Format: +   + +
Day, Date & Time Format: +   + +
Default Time Zone: + +   +
Daylight Saving: + >Observe daylight savings +
+

+ + +

+
diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php new file mode 100644 index 00000000..017eb29e --- /dev/null +++ b/include/staff/settings-emails.inc.php @@ -0,0 +1,109 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Email Settings

+ Note that some of the global settings can be overwritten at department/email level. +
Default System Email: + +   +
Default Alert Email: + +   +
Admin Email Address: + +  
Incoming Emails: For mail fetcher (POP/IMAP) to work you must set a cron job or enable auto-cron
Email Polling: > Enable POP/IMAP +   (Global setting which can be disabled at email level) +
Email Piping:> Enable email piping +   (You pipe we accept policy) +
Strip Quoted Reply: + > + (depends on the reply separator tag set below) +    +
Reply Separator Tag: +    +
Outgoing Emails: Default email only applies to outgoing emails without SMTP setting.
Default Outgoing Email: +     +
+

+ + +

+
diff --git a/include/staff/settings-general.inc.php b/include/staff/settings-general.inc.php new file mode 100644 index 00000000..ff5e058b --- /dev/null +++ b/include/staff/settings-general.inc.php @@ -0,0 +1,196 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

General Settings

+ Offline mode will disable client interface and only allow admins to login to Staff Control Panel +
Helpdesk Status: + />Online (Active) + />Offline (Disabled) +    +
Helpdesk URL: + +  
Helpdesk Name/Title: +  
Default Department: +   +
Default Email Templates: +   +
Default Page Size: + +
Default Log Level: + +   +
Purge Logs: + +
Password Reset Policy: + + +    +
Staff Excessive Logins: + failed login attempt(s) allowed before a + minute lock-out is enforced. +
Staff Session Timeout: + + Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). +
Bind Staff Session to IP: + > + (binds staff session to originating IP address upon login) +
Client Excessive Logins: + failed login attempt(s) allowed before a + minute lock-out is enforced. +
Client Session Timeout: + +  Maximum idle time in minutes before a client must log in again (enter 0 to disable). +
Clickable URLs: + > + (converts URLs in messages to clickable links) +
Enable Auto Cron: + > + (executes cron jobs based on staff activity - not recommended) +
+

+ + +

+
diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php new file mode 100644 index 00000000..2d368b3b --- /dev/null +++ b/include/staff/settings-kb.inc.php @@ -0,0 +1,37 @@ + +
+ + + + + + + + + + + + + + + + + +
+

Knowledgebase Settings

+ Disabling knowledgebase disables user's knowledgebase interface. +
Knowledgebase Status: + > + Enable Knowledgebase (Client Interface) +    +
Premade Responses: + > + Enable premade/canned responses (Available on ticket reply) +    +
+

+ + +

+
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php new file mode 100644 index 00000000..bcf7347a --- /dev/null +++ b/include/staff/settings-tickets.inc.php @@ -0,0 +1,140 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Ticket Settings and Options

+ Global ticket settings and options. +
Ticket IDs: + /> + Sequential + /> + Random (highly recommended) +
+ Default SLA: + + +   +
Default Priority: + +   +
Web Tickets Priority + > + (Allow user to overwrite/set priority) +
Emailed Tickets Priority + > + (Use email priority when available) +
Show Related Tickets + > + (Show all related tickets on user login - otherwise access is restricted to one ticket view per login) +
Human Verification: + > + Enable CAPTCHA on new web tickets.(requires GDLib)   
+
Maximum Open Tickets: + + per email/user. (Helps with spam and email flood control - enter 0 for unlimited) +
Ticket Auto-lock Time: + + + (Minutes to lock a ticket on activity - enter 0 to disable locking) +
Reopened Tickets: + > + Auto-assign reopened tickets to the last available respondent. +
Assigned Tickets: + > + Show assigned tickets on open queue. +
Answered Tickets: + > + Show answered tickets on open queue. +
Ticket Activity Log: + > + Log ticket activity as internal notes. +
Staff Identity Masking: + > + Hide staff's name on responses. +
+

+ + +

+
diff --git a/include/staff/settings.php b/include/staff/settings.php new file mode 100644 index 00000000..1dac69a5 --- /dev/null +++ b/include/staff/settings.php @@ -0,0 +1,721 @@ + +

System Preferences and Settings (v1.6 ST)

+ +
+
+Expand All | +Collapse All + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 General Settings

+ Offline mode will disable client interface and only allow super admins to login to Staff Control Panel +
+ Helpdesk Status: + + Online (Active) + Offline (Disabled) +    +
+ Helpdesk URL: + + +    +
+ Helpdesk Name/Title: + + +
+ Default E-Mail Templates: + + +    +
+ Default Department: + + +    +
+ Default Page Size: + + +
+ Default Log Level: + + +
+ Purge Logs: + + +
+ Excessive Staff Logins: + + failed login attempt(s) allowed before a + minute lock-out is enforced. +
+ Staff Session Timeout: + + +  Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). +
+ Staff Session IP Binding: + + + (binds staff session to originating IP address upon login) +
+ Excessive Client Logins: + + failed login attempt(s) allowed before a + minute lock-out is enforced. +
+ Client Session Timeout: + + +  Maximum idle time in minutes before a client must log in again (enter 0 to disable). +
+ Clickable URLs: + + + (converts URLs in messages to clickable links) +
+ Enable Auto-cron: + + + (executes cron jobs based on staff activity - not recommended) +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 Date and Time Settings

+ Please refer to PHP Manual for supported parameters. +
+ Time Format: + + +    + 09:24 AM +
+ Date Format: + + +    + 05/06/2011 +
+ Date & Time Format: + + +    + 05/06/2011 9:24 am +
+ Day, Date & Time Format: + + +   + Fri, May 6 2011 9:24am +
+ Default Timezone: + + +
+ Daylight Savings + + + observe daylight savings time +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 Ticket Options and Settings

+ If enabled ticket lock get auto-renewed on form activity. +
+ Ticket IDs: + + Sequential + Random (recommended) +
+ Ticket Priority: + +  Default Priority
+ + Allow user to overwrite/set priority (new web tickets)
+ + + Use email priority when available (new emailed tickets) +
+ Maximum Open Tickets: + + + per email (helps with spam and flood control - enter 0 for unlimited) +
+ Ticket Auto-lock Time: + + + (minutes to lock a ticket on activity - enter 0 to disable locking) +
+ Ticket Grace Period: + + + (hours before ticket is marked overdue - enter 0 to disable aging) +
+ Reopened Tickets: + + + Auto-assign reopened tickets to last available respondent. (3 months limit) +
+ Assigned Tickets: + + + Show assigned tickets on open queue. +
+ Answered Tickets: + + + Show answered tickets on open queue. +
+ Ticket Activity Log: + + + Log ticket activity as an internal note. +
+ Staff Identity Masking: + + + Hide staff's name on responses. +
+ Human Verification: + + + Enable CAPTCHA on new web tickets. + (requires GDLib) +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 E-mail Settings

+ Note that global settings can be disabled at dept/e-mail level. +
+ Incoming Email: +
For mail fetcher (POP/IMAP) to work you must set a cron job or enable auto-cron +
+ Enable POP/IMAP email fetch +  (Global setting which can be disabled at email level)
+ + Enable email piping +  (You pipe we accept policy)
+ + + Strip quoted reply (depends on the tag below)

+ + Reply Separator Tag: + +    +
+ Outgoing Email: +
Default Email: Only applies to outgoing emails with no SMTP settings.
+ +
+ + + +
+ Allow spoofing (No Overwrite). +
+
+ Default System E-Mail: + + +
+ Default Alert E-Mail: + + +
+ System Admin E-mail Address: + + +    +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+

 Autoresponders (Global Setting)

+ This is global setting which can be disabled at department level. +
+ New Ticket: + + Autoresponse includes the ticket ID required to check status of the ticket
+ Enable + Disable +

+
+ New Ticket by Staff: + + Notice sent when staff creates a ticket on behalf of the user (Staff can disable)
+ Enable + Disable +

+
+ New Message: + + Message appended to an existing ticket confirmation
+ Enable + Disable +

+
+ Ticket Denied: + + Ticket denied notice sent only once on limit violation to the user.
+ Enable + Disable + Note: Admin gets alerts on ALL denials by default. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 Alerts and Notices

+ Notices sent to user use 'No Reply Email' whereas alerts to staff use 'Alert Email' set above as FROM address respectively. +
+ New Ticket Alert: + + Enable + Disable +
+ Select recipients:  + Admin Email + Department Manager + Department Members (spammy) +
+ New Message Alert: + + Enable + Disable +
+ Select recipients:  + Last Respondent + Assigned Staff + Department Manager (spammy) +
+ New Internal Note Alert: + + Enable + Disable +
+ Select recipients:  + Last Respondent + Assigned Staff + Department Manager (spammy) +
+ Overdue Ticket Alert: + + Enable + Disable +
+ Select recipients: + Assigned Staff + Department Manager + Department Members (spammy) +
Note: Admin gets all overdue alerts by default. +
+ System Errors: + + System Errors + SQL errors + Excessive Login attempts +
Enabled errors are sent to admin email set above +
+

+ + +

+
+ + + + diff --git a/include/staff/slaplan.inc.php b/include/staff/slaplan.inc.php new file mode 100644 index 00000000..91d896d6 --- /dev/null +++ b/include/staff/slaplan.inc.php @@ -0,0 +1,103 @@ +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); +?> +
+ + + +

Service Level Agreement

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Tickets are marked overdue on grace period violation. +
+ Name: + + +   +
+ Grace Period: + + + ( in hours ) +   +
+ Status: + + >Active + >Disabled +   +
+ Priority Escalation: + + > + Enable priority escalation on overdue tickets. +
+ Ticket Overdue Alerts: + + > + Disable overdue alerts notices. (Overwrite global setting) +
+ Admin Notes: Internal notes.  +
+ +
+

+ + + +

+
diff --git a/include/staff/slaplans.inc.php b/include/staff/slaplans.inc.php new file mode 100644 index 00000000..4ebb46b3 --- /dev/null +++ b/include/staff/slaplans.inc.php @@ -0,0 +1,120 @@ +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!'; + +?> + +
+

Service Level Agreements

+
+
+ Add New SLA Plan
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="slas.php?&sort=name">Name href="slas.php?&sort=status">Status href="slas.php?&sort=period">Grace Period (hrs)href="slas.php?&sort=created">Date Addedhref="slas.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">  Disabled'; ?>   
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ + + +

+ +
+ diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php new file mode 100644 index 00000000..8883397f --- /dev/null +++ b/include/staff/staff.inc.php @@ -0,0 +1,289 @@ +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   *'; + //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); +?> +
+ + + +

Staff Account

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ', + $id,$checked,$name,($isactive?'':' (Disabled)')); + } + } ?> + + + + + + + +
+

+ User Information +
+ Username: + + +   +
+ First Name: + + +   +
+ Last Name: + + +   +
+ Email Address: + + +   +
+ Phone Number: + + +    + Ext +    +
+ Mobile Number: + + +    +
+ Account Password:    +
+ Password: + + +    +
+ Confirm Password: + + +    +
+ Forced Password Change: + + > + Force password change on next login. +
+ Staff's Signature: Optional signature used on outgoing emails.    +
+ +
Signature is made available as a choice, on ticket reply. +
+ Account Status & Settings: Dept. and assigned group controls access permissions. +
+ Account Type: + + > + Admin + >Staff +    +
+ Account Status: + + >Active + >Locked +    +
+ Assigned Group: + + +   +
+ Primary Department: + + +   +
+ User Default Time Zone: + + +   +
+ Limited Access: + + >Limit ticket access to ONLY assigned tickets. +
+ Directory Listing: + + >Show the user on staff's directory +
+ Vacation Mode: + + > + Staff on vacation mode. (No ticket assignment or alerts) +
+ Assigned Teams: Staff will have access to tickets assigned to a team they belong to regardless of the ticket's department. +
%s %s
+ Admin Notes: Internal notes viewable by all admins.  +
+ +
+

+ + + +

+
diff --git a/include/staff/staffmembers.inc.php b/include/staff/staffmembers.inc.php new file mode 100644 index 00000000..be8497b9 --- /dev/null +++ b/include/staff/staffmembers.inc.php @@ -0,0 +1,195 @@ +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; +?> +

Staff Members

+
+
+ + + + +    + +
+
+
Add New Staff
+
+showing(); +else + $showing='No staff found!'; +?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="staff.php?&sort=name">Name href="staff.php?&sort=username">UserName href="staff.php?&sort=status">Statushref="staff.php?&sort=group">Grouphref="staff.php?&sort=dept">Department href="staff.php?&sort=created">Created href="staff.php?&sort=login">Last Login
+ + onClick="highLight(this.value,this.checked);"> +  Locked'; ?> (vacation)':''; ?> 
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ +    + +    + +

+ +
+ diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php new file mode 100644 index 00000000..1cec6aff --- /dev/null +++ b/include/staff/syslogs.inc.php @@ -0,0 +1,176 @@ +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!'; +?> + +

System Logs

+
+
+
+ Date Span: +  From  + +    to    + + +    +  Type: + +    + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="syslogs.php?&sort=title">Log Title href="syslogs.php?&sort=type">Log Typehref="syslogs.php?&sort=date">Log Date href="syslogs.php?&sort=ip">IP Address
+ onClick="highLight(this.value,this.checked);">   
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ +

+ +
diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php new file mode 100644 index 00000000..7f8c409d --- /dev/null +++ b/include/staff/team.inc.php @@ -0,0 +1,120 @@ +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); +?> +
+ + + +

Team

+ + + + + + + + + + + + + + + + + + + + + + + + getMembers())){ ?> + + + + $staff){ + echo sprintf('', + $staff->getId(),$staff->getName(),$staff->getId()); + + + } + } ?> + + + + + + + +
+

+ Team Information: Disabled team won't be availabe for ticket assignment or alerts. +
+ Name: + + +   +
+ Status: + + >Active + >Disabled +   +
+ Team Lead: + + +    +
+ Assignment Alerts: + + > + Disable assignment alerts for this team (overwrite global settings.) +
+ Team Members: To add additional members go to target member's profile  +
+ %s +  Remove
+ Admin Notes: Internal notes viewable by all admins.  +
+ +
+

+ + + +

+
diff --git a/include/staff/teams.inc.php b/include/staff/teams.inc.php new file mode 100644 index 00000000..1aa6dc4e --- /dev/null +++ b/include/staff/teams.inc.php @@ -0,0 +1,125 @@ +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!'; + +?> +
+

Teams

+
+
+ Add New Team
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  href="teams.php?&sort=name">Team Name href="teams.php?&sort=status">Statushref="teams.php?&sort=members">Members href="teams.php?&sort=lead">Team Lead href="teams.php?&sort=created">Created href="teams.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">   Disabled'; ?>   + 0) { ?> + + 0 + +   +    
+ + Select:  + All   + None   + Toggle   + +
+ +

+ + + +

+ + +
+ diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php new file mode 100644 index 00000000..15881945 --- /dev/null +++ b/include/staff/template.inc.php @@ -0,0 +1,120 @@ +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); +?> +
+ + + +

Email Template

+ + + + + + + + + + + + + + + + + + + + + + + + $tpl){ + echo sprintf('', + $template->getId(),$k,Format::htmlchars($tpl['name']),Format::htmlchars($tpl['desc'])); + } + }else{ ?> + + + + + + + + + + + + +
+

+ Template information. +
+ Name: + + +   +
+ Status: + + >Active + >Disabled +   +
+ Language: + + +   +
+ Template Messages: Click on the message to edit.  + +
 %s - %s
+ Template To Clone: + + +   + (select an existing template to copy and edit it thereafter) +
+ Admin Notes: Internal notes.  +
+ +
+

+ + + +

+
diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php new file mode 100644 index 00000000..8ce8ca85 --- /dev/null +++ b/include/staff/templates.inc.php @@ -0,0 +1,127 @@ +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!'; + +?> + +
+

Email Templates

+
+
+ Add New Template
+
+
+ + + + + + + + + + + + + + + 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'])?'(System Default)':''; + ?> + + + + + + + + + + + + + + +
  href="templates.php?&sort=name">Name href="templates.php?&sort=status">Status href="templates.php?&sort=inuse">In-Usehref="templates.php?&sort=created">Date Addedhref="templates.php?&sort=updated">Last Updated
+ onClick="highLight(this.value,this.checked);">   +   Disabled'; ?>  Yes':'No'; ?>  
+ + Select:  + All   + None   + Toggle   + +
+ Page:'.$pageNav->getPageLinks().' '; +?> +

+ + + +

+ +
+ diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php new file mode 100644 index 00000000..a858c570 --- /dev/null +++ b/include/staff/ticket-open.inc.php @@ -0,0 +1,309 @@ +canCreateTickets()) die('Access Denied'); +$info=array(); +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); +?> +
+ + +

Open New Ticket

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + canAssignTickets()) { ?> + + + + + + canCloseTickets()) { ?> + + + + + + + + + + +
+

New Ticket

+ User Information +
+ Email Address: + + +   + notifyONNewStaffTicket()) { ?> +     + >Send alert to user. + +
+ Full Name: + + +   +
+ Phone Number: + + +    + Ext +    +
+ Ticket Information: +
+ Ticket Source: + + +  *  +
+ Department: + + +  *  +
+ Help Topic: + + +  *  +
+ Subject: + + +   +
+ Issue summary: Detailed summary of the reason(s) of opening the ticket. +
+ +
The user will be able to see the summary and any associated responses. +
+ Response: Optional response to the above issue. +
+ +
+ Canned Response:  + +     + +
+ + + allowAttachments()) { ?> +
Attachments: Response required when files are attached. +
+ $id) { + if(!($file=AttachmentFile::lookup($id))) continue; + $hash=$file->getHash().md5($file->getId().session_id().$file->getHash()); + echo sprintf(' ', + $file->getId(), $file->getId() , $hash, $file->getName()); + } + } + ?> +
+
+
+ +
+ +
+ Internal Note: Optional internal note (recommended on assignment)   +
+ +
+ Ticket Options: Due date, when set, overwrites SLA grace period. +
+ Due Date: + + + +    + +     + Time is based on your time zone (GM getTZoffset(); ?>) +
+ Priority: + + +    +
Assign To: +    +
+ Ticket Status: + + > + Close On Response (Only applicable if response is entered) +
Signature: + getDefaultSignatureType(); + ?> + + getSignature()) {?> + + + + +
+

+ + + +

+
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php new file mode 100644 index 00000000..f158a387 --- /dev/null +++ b/include/staff/ticket-view.inc.php @@ -0,0 +1,579 @@ +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.='  Ticket is assigned to '.$ticket->getAssignee().''; +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.='  Marked overdue!'; + +?> + + + + + +
+

Ticket #getExtId(); ?> + Reload

+
+ Print Ticket + Edit Ticket +
+ + + + + +
+ + + + + + + + + + + + + + + + + +
Status:getStatus()); ?>
Priority:getPriority(); ?>
Department:getDeptName()); ?>
Create Date:getCreateDate()); ?>
+
+ + + + + + + + + + + + + + + + + +
Name:getName()); ?>
Email: + getEmail(); + if(($related=$ticket->getRelatedTicketsCount())) { + echo sprintf('  (%d)', + urlencode($ticket->getEmail()),$related); + + } + ?> +
Phone:getPhoneNumber(); ?>
Source:getSource()); ?>
+
+
+ + + + + +
+ + + + + + + + + + isOpen()){ ?> + + + + + + + + + + +
Assigned To:getAssignee()); ?>
Last Response:getLastRespDate()); ?>
Due Date:getDueDate()); ?>
Close Date:getCloseDate()); ?>
+
+ + + + + + + + + + + + + +
Subject:getSubject(),200)); ?>
Help Topic:getHelpTopic()); ?>
Last Message:getLastMsgDate()); ?>
+
+
+ + +
+ getNumNotes() && ($notes=$ticket->getNotes())) { + foreach($notes as $note) { + + ?> + + + + + + + + + getAttachmentsLinks($note['note_id'],'N'))) {?> + + + + +
+ posted by %s', + Format::htmlchars($note['title']), + Format::htmlchars($note['source'])); + ?> +
+ +
+ No internal notes found.

"; + }?> +
+
+ getThreadCount() && ($messages = $ticket->getMessages())) { + foreach($messages as $message) {?> + + + + getAttachmentsLinks($message['msg_id'],'M'))) {?> + + + + +
+ getResponses($message['msg_id']))) { + foreach($responses as $resp) {?> + + + + + + + + getAttachmentsLinks($resp['response_id'],'R'))) {?> + + + + +
 - 
+ Error fetching ticket thread - get technical help.

'; + }?> +
+
+ +
+ +
+ +
+ + +
+ + +
+ + + + + + + + + + getDeptId()))) {?> + + + + + + + + + + allowAttachments()) { ?> + + + + + + + + + + isClosed() || $thisstaff->canCloseTickets()) { ?> + + + + + + +
 
+ + + +     + +
+ + + +
+ + +
+
+
+
+
+ +
+
+ + + getDefaultSignatureType(); + ?> + + getSignature()) {?> + + + canAppendSignature()) { ?> + + +
+ + + isClosed()) { ?> + + canCloseTickets()) { ?> + + +
+

+ + +

+
+
+ + + + + + + + + + + + + + + + + allowAttachments()) { ?> + + + + + + + + + + +
 
+ + + +   +
+ + +
Internal note details  +
+ +
+ + +
+
+
+ +
+
+ + + isClosed()){ ?> + + canCloseTickets()) { ?> + + isAnswered()) { ?> + + + + +
+ +

+ + +

+
+ canTransferTickets()) { ?> +
+ + + + + + + + + + + + + + + +
 
+ + +   +
+ + + Enter reasons for the transfer. +
+ +
+

+ + +

+
+ + canAssignTickets()) { ?> +
+ + + + + + + + + + + + + + + +
  + isAssigned()) + echo sprintf('Ticket is currently assigned to %s',$ticket->getAssignee()); + ?> +
+ + +   +
+ + + Enter reasons for the assignment or instructions. +
+ +
+

+ + +

+
+ +
+ diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php new file mode 100644 index 00000000..dad492a1 --- /dev/null +++ b/include/staff/tickets.inc.php @@ -0,0 +1,528 @@ +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. +?> + +
+
+ + + + + + +
+
+
+
+
+ + + + + + + + +
Query: Dept: + Status is: + + +
+
+ Date Span: +  From  + +    to    + + +    +
+ + + + + + + + +
Type: + + + + Sort by: + + + + Results Per Page: + + + +  [ Basic ] +
+
+
+ +
+
+
+ Refresh + + + + + + + canManageTickets()) { ?> + + + + + + + + + + + + + + + + + + + + + %s',Format::truncate($row['staff'],40)); + elseif($row['team_id']) + $lc=sprintf('%s',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('%s',$tid); + } + ?> + + canManageTickets()) { ?> + + + + + + + $displaystatus"; + echo ""; + } else { ?> + + + + + + + + + + + +
   
  + href="tickets.php?sort=ID&order=" + title="Sort By Ticket ID ">Ticket + href="tickets.php?sort=date&order=" + title="Sort By Date ">Date + href="tickets.php?sort=subj&order=" + title="Sort By Subject ">Subject + href="tickets.php?sort=name&order=" + title="Sort By Name ">From + href="tickets.php?sort=status&order=" + title="Sort By Status ">Status> + href="tickets.php?sort=pri&order=" + title="Sort By Priority ">Priority + href="tickets.php?sort=assignee&order=" + title="Sort By Assignee ">Assigned To + href="tickets.php?sort=staff&order=" + title="Sort By Closing Staff Name ">Closed By + href="tickets.php?sort=dept&order=" + title="Sort By Department ">Department
+ + + class="Icon Ticket" title=" Ticket" + href="tickets.php?id="> +   + 1)?" ($threadcount) ":''?> +  ":''; ?> +   $displaystatus +  
+ + Select:  + All   + None   + Toggle   + +
+ 0){ //if we actually had any tickets returned. + echo '
 Page:'.$pageNav->getPageLinks().' 
'; + ?> + canManageTickets()) { ?> +

+ + + + + + + + + canDeleteTickets()) { ?> + + +

+ +
+
diff --git a/include/staff/topic.inc.php b/include/staff/topic.inc.php new file mode 100644 index 00000000..6504c544 --- /dev/null +++ b/include/staff/topic.inc.php @@ -0,0 +1,80 @@ +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); +?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Disabling auto response will overwrite dept settings.
Help Topic: +  
Topic Status + />Active + />Disabled +
Auto Response: + > + Disable autoresponse for this topic. (Overwrite Dept setting) +
New Ticket Priority: +   +
New Ticket Department: +   +
+
+ + + +
+
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php new file mode 100644 index 00000000..43412bca --- /dev/null +++ b/include/staff/tpl.inc.php @@ -0,0 +1,63 @@ +getMsgTemplate($info['tpl']),$info); + +?> +

Email Template Message - getName(); ?>

+
+
+ + + Message Template: + + +     +
+
+
+ + + + + + + + + + + + + + + + + + + +
+

+ Subject and body required. Supported Variables. +
+ Message Subject: Email message subject
+ +
+ Message Body: Email message body.
+ +
+

+ + + +

+
diff --git a/index.php b/index.php new file mode 100644 index 00000000..37e5c7f1 --- /dev/null +++ b/index.php @@ -0,0 +1,16 @@ + + 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 00000000..f78f96a1 --- /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=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;ic)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(;j0)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(;g1?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
a";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="";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="
",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
";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-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=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=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-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;i0?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;g0){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+~,(\[\\]+)+|[>+~])(\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;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=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 bc[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=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.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.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="

";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="
";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;g0)for(var g=c;g0},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-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-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=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_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
","
"]),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>");try{for(var c=0,e=this.length;c1&&l0?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>");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]===""&&!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>/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("
").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=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
";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 00000000..fd04ff48 --- /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('
'); + 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 00000000..e0c1661c --- /dev/null +++ b/kb/faq.php @@ -0,0 +1,36 @@ + + 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 00000000..80d88170 --- /dev/null +++ b/kb/file.php @@ -0,0 +1,30 @@ + + 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 00000000..1963d391 --- /dev/null +++ b/kb/index.php @@ -0,0 +1,22 @@ + + 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 00000000..9811e63c --- /dev/null +++ b/kb/kb.inc.php @@ -0,0 +1,26 @@ + + 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 00000000..6fd211ed --- /dev/null +++ b/main.inc.php @@ -0,0 +1,190 @@ + + 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('Error loading settings. Contact admin.'); + + 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("Fatal Error: 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 00000000..3c3b24a4 --- /dev/null +++ b/offline.php @@ -0,0 +1,25 @@ + + 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'); +?> +
+

Support Ticket System Offline

+

Thank you for your interest in contacting us.

+

Our helpdesk is offline at the moment, please check back at a later time.

+
+ diff --git a/scp/admin.inc.php b/scp/admin.inc.php new file mode 100644 index 00000000..a580fa69 --- /dev/null +++ b/scp/admin.inc.php @@ -0,0 +1,55 @@ + + 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 upgrade.'; + $errors['err']=$sysnotice; +}elseif(!$cfg->isHelpDeskOffline()) { + if(file_exists('../setup/')){ + $sysnotice='Please take a minute to delete setup/install 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 chmod 644 %s', + 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 00000000..d84edfed --- /dev/null +++ b/scp/admin.php @@ -0,0 +1,4 @@ + diff --git a/scp/ajax.php b/scp/ajax.php new file mode 100644 index 00000000..a65de32d --- /dev/null +++ b/scp/ajax.php @@ -0,0 +1,59 @@ + + 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\d+).(?Pjson|txt)", "cannedResp"), + url_get("^faq/(?P\d+)","faq") + )), + url("^/content/", patterns("ajax.content.php:ContentAjaxAPI", + url_get("^log/(?P\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\d+)/preview", "previewTicket"), + url_get("^(?P\d+)/lock", "acquireLock"), + url_post("^(?P\d+)/lock/(?P\d+)/renew", "renewLock"), + url_post("^(?P\d+)/lock/(?P\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 00000000..ece24444 --- /dev/null +++ b/scp/apikeys.php @@ -0,0 +1,101 @@ + + 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 00000000..4b0b2c31 --- /dev/null +++ b/scp/attachment.php @@ -0,0 +1,31 @@ + + 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 00000000..589ea416 --- /dev/null +++ b/scp/autocron.php @@ -0,0 +1,44 @@ + + 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 00000000..1ef580a5 --- /dev/null +++ b/scp/banlist.php @@ -0,0 +1,124 @@ + + 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 DISABLED - enable here.'; + +$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 00000000..1cb77bc0 --- /dev/null +++ b/scp/canned.php @@ -0,0 +1,126 @@ + + 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 00000000..787b81b5 --- /dev/null +++ b/scp/categories.php @@ -0,0 +1,107 @@ + + 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 00000000..8516ea0e --- /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 00000000..85a4712c --- /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=> text */ + +.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 00000000..05516395 --- /dev/null +++ b/scp/dashboard.php @@ -0,0 +1,22 @@ + + 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 00000000..d0869cdf --- /dev/null +++ b/scp/departments.php @@ -0,0 +1,107 @@ + + 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 00000000..db5b64ce --- /dev/null +++ b/scp/directory.php @@ -0,0 +1,22 @@ + + 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 00000000..ddc626fb --- /dev/null +++ b/scp/emails.php @@ -0,0 +1,86 @@ + + 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 00000000..9f9b2dec --- /dev/null +++ b/scp/emailtest.php @@ -0,0 +1,120 @@ + + 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'); +?> +
+ +

Test Outgoing Email

+ + + + + + + + + + + + + + + + + + + + + + + +
+ Emails delivery depends on your server settings (php.ini) and/or email SMTP configuration. +
+ From: + + +   +
+ To: + + +   +
+ Subject: + + +   +
+ Message: email message to send. 
+ +
+

+ + + +

+
+ diff --git a/scp/faq.php b/scp/faq.php new file mode 100644 index 00000000..f668be9f --- /dev/null +++ b/scp/faq.php @@ -0,0 +1,114 @@ + + 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 00000000..61bc7da1 --- /dev/null +++ b/scp/file.php @@ -0,0 +1,30 @@ + + 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 00000000..f39b794d --- /dev/null +++ b/scp/filters.php @@ -0,0 +1,104 @@ + + 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 00000000..7a4f9591 --- /dev/null +++ b/scp/groups.php @@ -0,0 +1,96 @@ + + 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 00000000..18c34393 --- /dev/null +++ b/scp/helptopics.php @@ -0,0 +1,101 @@ + + 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 00000000..313b6273 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 00000000..0ad2aa18 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 00000000..3b30b3c5 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 00000000..fac668fc 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 00000000..322d0b6e 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 00000000..819ffbb6 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 00000000..8526cf5d 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 00000000..6cff76c1 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 00000000..74157867 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 00000000..dc3e1d5c 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 00000000..d1f6a400 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 00000000..620553e9 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 00000000..8892a55a 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 00000000..2defcfcf 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 00000000..2defcfcf 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 00000000..6c21c678 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 00000000..9d5b44ea 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 00000000..9fda911a 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 00000000..6d81d333 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 00000000..373b8d58 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 00000000..9ea1516e 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 00000000..c1ac8793 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 00000000..bff388fe 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 00000000..df7b54d2 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 00000000..b2c0deaa 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 00000000..b2c0deaa 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 00000000..25fd5d32 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 00000000..7038ed29 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 00000000..44ab26dd 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 00000000..632867e2 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 00000000..f8e05f6e 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 00000000..dc4e198b 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 00000000..9544721f 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 00000000..495e8b9d 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 00000000..00579559 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 00000000..a11afcfd 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 00000000..4400e61e 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 00000000..526d30a5 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 00000000..28dc32f3 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 00000000..2676e91c 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 00000000..a4d178e5 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 00000000..1c752a9b 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 00000000..373b8d58 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 00000000..619279d3 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 00000000..bb52f61c 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 00000000..4b6fdece 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 00000000..4914e85f 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 00000000..10a29ef0 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 00000000..32c941aa 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 00000000..32c941aa 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 00000000..0a675850 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 00000000..d6b060d1 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 00000000..e2eae94f 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 00000000..b856f4db 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 00000000..97b20e88 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 00000000..a71518d7 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 00000000..cd1526c3 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 00000000..e0fcdfe5 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 00000000..1782e843 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 00000000..939353b7 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 00000000..5b01ff80 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 00000000..e2e5b0ba 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 00000000..eb913d13 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 00000000..552211ca 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 00000000..0a675850 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 00000000..97478ca3 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 00000000..17b9f418 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 00000000..7a5d21fd 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 00000000..83e025eb 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 00000000..83e025eb 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 00000000..6f3067ff 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 00000000..4d572a50 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 00000000..c24bf877 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 00000000..2d9d7529 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 00000000..524e0e4f 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 00000000..c77f40e4 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 00000000..c02a6f47 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 00000000..261cab9b 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 00000000..fd4e4335 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 00000000..691aa495 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 00000000..f6b84187 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 00000000..bffd6b0b 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 00000000..4304ea79 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 00000000..c5513841 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 00000000..daa3b807 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 00000000..76e231f1 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 00000000..981b5039 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 00000000..261cab9b 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 00000000..5cf1ebed 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 00000000..0cf40c72 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 00000000..f3a4df2d 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 00000000..06896195 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 00000000..13b048f7 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 00000000..16262493 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 00000000..4df4e214 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 00000000..dabfde5d --- /dev/null +++ b/scp/index.php @@ -0,0 +1,18 @@ + + 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 00000000..3a9c8760 --- /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(''); +document.write(''); +document.write(''); +for(var kk=1;kk<=6;kk++) { + document.write(''); + for(var tt=1;tt<=7;tt++) { + num=7 * (kk-1) - (-tt); + document.write(''); + } + document.write(''); +} +document.write(''); + +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= (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 00000000..86cdffd4 --- /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.nicEdit-main p { margin: 0; } + + + + +
+ +
diff --git a/setup/inc/install-done.inc.php b/setup/inc/install-done.inc.php new file mode 100644 index 00000000..3aaf5caf --- /dev/null +++ b/setup/inc/install-done.inc.php @@ -0,0 +1,48 @@ + +
+

Congratulations!

+
+

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.

+ +

Config file permission:

+ Change permission of ost-config.php to remove write access as shown below. +
    +
  • CLI:
    chmod 0664 include/ost-config.php
  • +
  • FTP:
    Using WS_FTP this would be right hand clicking on the file, selecting chmod, and then remove write access
  • +
  • Cpanel:
    Click on the file, select change permission, and then remove write access.
  • +
+
+

Below, you'll find some useful links regarding your installation.

+ + + + + + + + + + +

PS: Don't just make customers happy, make happy customers!

+
+ diff --git a/setup/inc/install-prereq.inc.php b/setup/inc/install-prereq.inc.php new file mode 100644 index 00000000..b07ffd99 --- /dev/null +++ b/setup/inc/install-prereq.inc.php @@ -0,0 +1,41 @@ + + +
+

Thank You for Choosing osTicket!

+
+

We are delighted you have chosen osTicket for your customer support ticketing system!

+

The installer will guide you every step of the way in the installation process. You're minutes away from your awesome customer support system!

+
+

Prerequisites.

+

Before we begin, we'll check your server configuration to make sure you meet the minimum requirements to install and run osTicket.

+

Required:

+ These items are necessary in order to install and use osTicket. +
    +
  • + PHP v4.3 or greater - ()
  • +
  • + MySQL v4.4 or greater - ()
  • +
+

Recommended:

+ You can use osTicket without these, but you may not be able to use all features. +
    +
  • Mcrypt extension
  • +
  • Gdlib extension
  • +
  • PHP IMAP extension
  • +
+
+
+ + +
+
+
+ diff --git a/setup/inc/install.inc.php b/setup/inc/install.inc.php new file mode 100644 index 00000000..5daefe12 --- /dev/null +++ b/setup/inc/install.inc.php @@ -0,0 +1,120 @@ +'ost_','dbhost'=>'localhost'); + +//XXX: Remove b4 release. +if($_SESSION['installer']['info'] && !$_POST) + $info=$_SESSION['installer']['info']; + + +?> +
+

osTicket Basic Installation

+

Please fill out the information below to continue your osTicket installation. All fields are required.

+ +
+ +

System Settings

+ The URL of your helpdesk, its name, and the default system email address +
+ + +
+
+ + + ? + +
+
+ + + ? + +
+ +

Admin User

+ Your primary administrator account - you can add more users later. +
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+ +

Database Settings

+ Database connection information +
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+ + + ? + +
+
+
+ +
+
+
+
+

Need Help? We provide professional installation services and commercial support. Learn More!

+
+
+
+

Doing stuff!

+ Please wait... while we install your new support ticket system! +
diff --git a/setup/inc/msg/installed.txt b/setup/inc/msg/installed.txt new file mode 100644 index 00000000..58bbeb5a --- /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 00000000..d88a2973 --- /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 00000000..8a1f3b98 --- /dev/null +++ b/setup/inc/ost-sampleconfig.php @@ -0,0 +1,47 @@ + + 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 00000000..7dcf180b --- /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 00000000..a9966d53 --- /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 00000000..9f44710f --- /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 00000000..eb7442a5 --- /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 00000000..571620fb --- /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 00000000..6464a100 --- /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 00000000..45dddf1d --- /dev/null +++ b/setup/inc/subscribe.inc.php @@ -0,0 +1,48 @@ + +
+

Basic Installation Completed

+

osTicket installation has been completed successfully.

+

Stay up to date:

+ It's important to keep your installation up to date. Get announcements, security updates and alerts delivered directly to you! +

+
+ +
+ + + +
+
+ + + +
+
+
+ I'd like to receive the following notifications: + + +
+
+ + No thanks. +
+
+
+ diff --git a/setup/inc/upgrade-aborted.inc.php b/setup/inc/upgrade-aborted.inc.php new file mode 100644 index 00000000..8d8f64aa --- /dev/null +++ b/setup/inc/upgrade-aborted.inc.php @@ -0,0 +1,27 @@ + +
+

Upgrade Aborted!

+
+

Upgrade aborted due to errors. Any errors at this stage are fatal. Please note the error(s), if any, when contacting us.

+ %s',$errors['err']); + + echo '

    '; + unset($errors['err']); + foreach($errors as $k=>$error) + echo sprintf('
  • %s
  • ',$error); + echo '
'; + } ?> +

Please, refer to the Upgrade Guide on the wiki for more information.

+
+

Need Help? We provide professional upgrade services and commercial support. Contact us today for expedited help.

+
+ diff --git a/setup/inc/upgrade-attachments.inc.php b/setup/inc/upgrade-attachments.inc.php new file mode 100644 index 00000000..1a695264 --- /dev/null +++ b/setup/inc/upgrade-attachments.inc.php @@ -0,0 +1,32 @@ + +
+

Attachments Migration

+

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.

+

We have to migrate files in batches for technical reasons.

+

Please don't cancel or close the browser.

+ +
+
+ + +
+ +
+
+ +
+
+

Moving attachments

+
+ Please wait... while we migrate attachments! +

+
+
diff --git a/setup/inc/upgrade-cleanup.inc.php b/setup/inc/upgrade-cleanup.inc.php new file mode 100644 index 00000000..f8406ae5 --- /dev/null +++ b/setup/inc/upgrade-cleanup.inc.php @@ -0,0 +1,38 @@ + +
+

osTicket Upgrade

+
+

We're almost done! Please don't cancel or close the browser, any errors at this stage will be fatal.

+
+

Cleanup: Step 2 of 2

+

The upgrade wizard will now attempt to do post upgrade cleanup. It might take a while dending on the size of your database.

+
    +
  • Setting Changes
  • +
  • Attachment Migration
  • +
  • Database Optimization
  • +
+
+
+ + +
+
+
+ + +
+
+

Doing serious stuff!

+
+ Please wait... while we do post-upgrade cleanup! +

+
+
diff --git a/setup/inc/upgrade-core.inc.php b/setup/inc/upgrade-core.inc.php new file mode 100644 index 00000000..ec42e504 --- /dev/null +++ b/setup/inc/upgrade-core.inc.php @@ -0,0 +1,38 @@ + +
+

osTicket Upgrade

+
+

Thank you for taking the time to upgrade your osTicket intallation!

+

Please don't cancel or close the browser, any errors at this + stage will be fatal.

+
+

Base upgrade: Step 1 of 2

+

The upgrade wizard will now attempt to upgrade your database and core settings!

+
    +
  • Database enhancements
  • +
  • New and updated features
  • +
  • Enhance settings and security
  • +
+
+
+ + +
+
+
+ + +
+
+

Doing serious stuff!

+ Please wait... while we upgrade your osTicket installation! +

Smile!
+
diff --git a/setup/inc/upgrade-done.inc.php b/setup/inc/upgrade-done.inc.php new file mode 100644 index 00000000..0ec75c31 --- /dev/null +++ b/setup/inc/upgrade-done.inc.php @@ -0,0 +1,30 @@ + +
+

Upgrade Completed!

+
+

Congratulations osTicket upgrade has been completed successfully. Please refer to Release Notes for more information about changes and/or new features.

+

Post-upgrade manual cleanup

+ Please take a minute to cleanup! +
    +
  • Delete setup directory:
    After verifying that the upgrade completed correctly please delete setup folder.
  • +
  • Enable the helpdesk:
    You can now enable osTicket, be sure to take some time to explore changes and new features
  • +
+
+

Once again, thank you for choosing osTicket. Your feedback is welcomed!

+

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.

+

Good luck.

+

osTicket Team.

+
+

PS: Don't just make customers happy, make happy customers!

+
+ diff --git a/setup/inc/upgrade.inc.php b/setup/inc/upgrade.inc.php new file mode 100644 index 00000000..e5b8ba77 --- /dev/null +++ b/setup/inc/upgrade.inc.php @@ -0,0 +1,50 @@ + +
+

osTicket Upgrade!

+ +
+

Thank you for being a loyal osTicket user!

+

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.

+
+

Getting ready!

+

Before we begin, we'll check your server configuration to make sure you meet the minimum requirements to run the latest version of osTicket.

+

Prerequisites:

+ These items are necessary in order to use osTicket the latest version of osTicket. +
    +
  • + PHP v4.3 or greater - ()
  • +
  • + MySQL v4.4 or greater - ()
  • +
+

Higly Recommended:

+ We hightly recommend that you follow the steps below. +
    +
  • Take osTicket offline momentarily during upgrade.
  • +
  • Backup the current database, if you haven't done so already.
  • +
  • Be patient the upgrade process will take a couple of seconds.
  • +
+
+
+ + +
+
+
+ + +
+
+

Doing stuff!

+ Please wait... while we upgrade your osTicket installation! +
+
diff --git a/setup/index.php b/setup/index.php new file mode 100644 index 00000000..36664d8a --- /dev/null +++ b/setup/index.php @@ -0,0 +1,3 @@ + diff --git a/setup/install.php b/setup/install.php new file mode 100644 index 00000000..e98c7aa7 --- /dev/null +++ b/setup/install.php @@ -0,0 +1,111 @@ + + 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 00000000..48590ecb --- /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"?"":"")+""),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;g0){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=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;ic)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(;j0)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(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?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
a",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="
",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",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-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=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=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-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;i0?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;g0){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+~,(\[\\]+)+|[>+~])(\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;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=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 bc[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=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",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.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="

";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="
";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;g0)for(h=g;h0: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-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-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:]+)/,_=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_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
","
"]),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>");try{for(var c=0,d=this.length;c1&&l0?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>");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]===""&&!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=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>/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("
").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=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
";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 00000000..5d274ea3 --- /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 00000000..f24d8251 --- /dev/null +++ b/setup/js/tips.js @@ -0,0 +1,60 @@ +jQuery(function($) { + var tips = $('.tip'); + for(i=0;i').attr('src', './images/tip_arrow.png').addClass('tip_arrow'); + var tip_box = $('
').addClass('tip_box'); + var tip_content = $('
').addClass('tip_content').load('tips.html '+elem.attr('href'), function() { + tip_content.prepend('x'); + }); + + 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 00000000..7fc66597 --- /dev/null +++ b/setup/setup.inc.php @@ -0,0 +1,53 @@ + + 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 00000000..1cd8de93 --- /dev/null +++ b/setup/tips.html @@ -0,0 +1,52 @@ +
+Helpdesk Name +

The name of your support system e.g [Company Name] Support

+
+
+Default System Email +

Default email address e.g support@yourcompany.com - you can add more later!

+
+
+First Name +

Admin's first name

+
+
+Last Name +

Admin's last name

+
+
+Email Address +

Admin's personal email address. Must be different from system's default email.

+
+
+Username +

Admin's login name. Must be at least three (3) characters.

+
+
+Password +

Admin's password. Must be five (5) characters or more.

+
+
+Confirm Password +

Retype admin's password. Must match.

+
+
+MySQL Table Prefix. +

osTicket requires table prefix in order to avoid possible table conflicts in a shared database.

+
+
+MySQL Hostname +

Most hosts use 'localhost' for local database hostname. Check with your host if localhost fails. Default port set in php.ini is assumed.

+
+
+MySQL Database +

Name of the database osTicket will use.

+
+
+MySQL Username +

The MySQL user must have full rights to the database.

+
+
+MySQL Password +

MySQL password associated with above user.

+
diff --git a/setup/upgrade.php b/setup/upgrade.php new file mode 100644 index 00000000..6fc34cfd --- /dev/null +++ b/setup/upgrade.php @@ -0,0 +1,17 @@ + + 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.'); +?>