diff --git a/.gitignore b/.gitignore index 2c0568a588f2c2759a62bc94151c0597f8c09abe..b5c98c948d9c2eb75df122268b315b29f15a9e93 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ php53.cgi include/ost-config.php *.sw[a-z] .DS_Store +.vagrant +Vagrantfile diff --git a/README.md b/README.md index 4d97925949c0c7e4fff49c52ab5c8b445572f171..14893ac9f41cfb17429b42c86da86bbe33904248 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,10 @@ file for the gory details of the General Public License. osTicket is supported by several magical open source projects including: - * [FPDF] (http://www.fpdf.org/) + * [Font-Awesome](http://fortawesome.github.com/Font-Awesome/) + * [FPDF](http://www.fpdf.org/) * [HTMLawed](http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed) + * [jQuery dropdown](http://labs.abeautifulsite.net/jquery-dropdown/) * [PasswordHash](http://www.openwall.com/phpass/) * [PEAR](http://pear.php.net/package/PEAR) * [PEAR/Auth_SASL](http://pear.php.net/package/Auth_SASL) diff --git a/WHATSNEW.md b/WHATSNEW.md index 8cac25f814492d4a1790a948bbf959165d536223..afe5476bb591121a4c58a81881e0df7e644aab2d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,3 +1,11 @@ +New stuff in 1.7-rc3 +==================== + * Bug fixes from rc2 + * Canned auto-reply template + * Modal dialogs + * PEAR packages upgrade + * Email encoding + New stuff in 1.7-rc2 ==================== * Bug fixes from rc1 diff --git a/assets/default/about-custom-themes.md b/assets/default/about-custom-themes.md new file mode 100644 index 0000000000000000000000000000000000000000..eecf1bbbed3d3002d3d4abe686f8ec6d82717094 --- /dev/null +++ b/assets/default/about-custom-themes.md @@ -0,0 +1,14 @@ +Customizng Your Theme +==== + +When modifying the default theme, it is recommended that you do not +edit the CSS files directly. Instead, you should use the included +LESS files and recompile the CSS file with your edits. + +Even if you decide to edit the CSS directly, we recommend that you +keep the LESS files, so that you can quickly rebuild the default +theme should you need to recover it, or for upgrade purposes. + +*Please do not submit any CSS edits to the official branch, unless +they are done within the LESS files.* + diff --git a/assets/default/css/print.css b/assets/default/css/print.css index 9f1c25746ca0486d2524e24ea850b1d2ffcd9e6c..aca3d50f9265e03c3a95d5a5dbbe78aa891b0a21 100644 --- a/assets/default/css/print.css +++ b/assets/default/css/print.css @@ -1,29 +1 @@ -#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; -} +#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} \ No newline at end of file diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index b59f6af8955857d2008dff647e8e4d83d4924e72..9780f0e048f7598682038fd90f876af17c3f77ed 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -1,31 +1,29 @@ -/* 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 { +body, +input, +select, +textarea { font-family: sans-serif; color: #000; } - -b, strong { +b, +strong { font-weight: bold; } - blockquote { margin: 1em 40px; } - hr { display: block; height: 1px; @@ -34,119 +32,120 @@ hr { margin: 1em 0; padding: 0; } - small { font-size: 85%; } - -ul, ol { +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 { +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"] { +input[type="button"], +input[type="reset"], +input[type="submit"] { cursor: pointer; -webkit-appearance: button; } - -input[type="checkbox"], input[type="radio"] { +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; } - -th, td { +th, +td { vertical-align: top; } - -th { text-align: left; font-weight: normal; } - -h1, h2, h3, h4, h5, h6, form, fieldset { +th { + text-align: left; + font-weight: normal; +} +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;} - +.centered { + text-align: center; +} +.clear { + clear: both; + height: 1px; + visibility: none; +} +.hidden { + display: none; +} +.faded { + color: #666; +} /* Pagination */ #pagination { border: 0; @@ -161,24 +160,26 @@ h2 { list-style: none; display: inline; } -#pagination a { +#pagination li a { margin-right: 2px; display: block; float: left; padding: 3px 6px; text-decoration: none; } -#pagination a:hover { +#pagination li a:hover { color: #ff0084; } -#pagination .previousOff, #pagination .nextOff { +#pagination .previousOff, +#pagination .nextOff { color: #666; display: block; float: left; font-weight: bold; padding: 3px 4px; } -#pagination .next a, #pagination .previous a { +#pagination .next a, +#pagination .previous a { font-weight: bold; } #pagination .active { @@ -190,16 +191,34 @@ h2 { 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; } - - +#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; @@ -209,19 +228,64 @@ h2 { color: #a00; font-style: normal; } - .error { - color:#f00; + color: #f00; } - .error input { - border:1px solid #f00;} - -/* Main layout */ + border: 1px solid #f00; +} +.button, +.button:visited { + background: #222; + display: inline-block; + font-size: 16px; + padding: 8px 16px 6px 16px; + width: 160px; + text-align: center; + color: #fff; + font-weight: bold; + text-decoration: none; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + position: relative; + cursor: pointer; + font-family: helvetica, arial, sans-serif; +} +.button:hover { + background-color: #111; + color: #fff; +} +.button:active { + top: 1px; + box-shadow: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; +} +.button, +.button:visited, +.green.button, +.green.button:visited { + background-color: #91bd09; +} +.green.button:hover { + background-color: #749a02; +} +.blue.button, +.blue.button:visited { + background-color: #00AEEF; +} +.blue.button:hover { + background-color: #0299d2; +} body { background: url('../images/page_bg.png') top left repeat-x #c8c8c8; } - #container { background: #fff; width: 840px; @@ -230,7 +294,6 @@ body { -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; @@ -241,7 +304,6 @@ body { height: 71px; float: left; } - #header p { width: 400px; text-align: right; @@ -249,16 +311,15 @@ body { 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; - box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); - -webkit-box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); + box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4); + -moz-box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4); + -webkit-box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4); } #nav li { margin: 0; @@ -282,7 +343,8 @@ body { background-position: 10px 50%; background-repeat: no-repeat; } -#nav li a.active, #nav li a:hover { +#nav li a.active, +#nav li a:hover { background-color: #dbefff; color: #000; } @@ -305,7 +367,6 @@ body { #nav li a.tickets { background-image: url('../images/icons/tix.png'); } - #content { padding: 20px 0; margin: 0 20px; @@ -313,7 +374,6 @@ body { height: 350px; min-height: 350px; } - #footer { text-align: center; font-size: 11px; @@ -322,7 +382,6 @@ body { #footer a { color: #333; } - #footer p { margin: 10px 0 0 0; } @@ -333,45 +392,23 @@ body { outline: none; text-indent: -9999px; margin: 0 auto; - background: url('../images/poweredby.png?1319571688') top left no-repeat; + background: url('../images/poweredby.png') 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; + background: url('../images/new_ticket_icon.png') 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; + background: url('../images/check_status_icon.png') top left no-repeat; } -#landing_page form input[type=submit] { - display: block; - width: 192px; - height: 38px; - border: none; - margin: 0; - padding: 0; - text-indent: -9999px; -} - +/* Landing page FAQ not yet implemented. */ #faq { clear: both; margin: 0; @@ -381,136 +418,129 @@ body { font-size: 15px; margin-left: 0; padding-left: 0; - border-top:1px solid #ddd; + border-top: 1px solid #ddd; } #faq ol li { list-style: none; margin: 0; - padding:0; + padding: 0; color: #999; } #faq ol li a { - display:block; - padding:5px 0; - height:auto !important; - overflow:hidden; - margin:0; - border-bottom:1px solid #ddd; + display: block; + padding: 5px 0; + height: auto !important; + overflow: hidden; + margin: 0; + border-bottom: 1px solid #ddd; line-height: 16px; padding-left: 24px; - background: url('../images/icons/page.png?1319579499') 0 50% no-repeat; + background: url('../images/icons/page.png') 0 50% no-repeat; } #faq ol li a:hover { - background-color:#e9f5ff; + background-color: #e9f5ff; } - -.article-meta { - padding:5px; - background:#fafafa; +#faq .article-meta { + padding: 5px; + background: #fafafa; } - -/* Knowledgebase */ #kb { margin: 2px 0; padding: 5px; overflow: hidden; } - #kb > li { - padding:10px; - height:auto !important; - overflow:hidden; - margin:0; - background:url(../images/kb_category_bg.png) bottom left repeat-x; - border-bottom:1px solid #ddd; -} - -#kb li i { - display:block; - width:32px; - height:32px; - float:left; - margin-right:6px; - background:url(../images/kb_large_folder.png) top left no-repeat; -} - + padding: 10px; + height: auto !important; + overflow: hidden; + margin: 0; + background: url(../images/kb_category_bg.png) bottom left repeat-x; + border-bottom: 1px solid #ddd; +} #kb > li h4 { - padding-bottom:3px; - margin-bottom:3px; + padding-bottom: 3px; + margin-bottom: 3px; } - #kb > li h4 span { - color:#666; + color: #666; } - #kb > li h4 a { font-size: 14px; } - +#kb li i { + display: block; + width: 32px; + height: 32px; + float: left; + margin-right: 6px; + background: url(../images/kb_large_folder.png) top left no-repeat; +} #kb-search { - padding:10px 0; - overflow:hidden; + padding: 10px 0; + overflow: hidden; } - #kb-search div { - clear:both; - overflow:hidden; - padding-top:5px; + clear: both; + overflow: hidden; + padding-top: 5px; } - #kb-search #query { - margin:0; - display:inline-block; - float:left; - width:200px; - margin-right:5px; + margin: 0; + display: inline-block; + float: left; + width: 200px; + margin-right: 5px; } - #kb-search #cid { - margin:0; - display:inline-block; - float:left; - width:200px; - margin-right:5px; - position:relative; - top:2px; -} - + margin: 0; + display: inline-block; + float: left; + width: 200px; + margin-right: 5px; + position: relative; + top: 2px; +} #kb-search #topic-id { - margin:0; - display:inline-block; - float:left; - width:410px; + margin: 0; + display: inline-block; + float: left; + width: 410px; } - #kb-search #searchSubmit { - margin:0; - display:inline-block; - float:left; - position:relative; - top:2px; -} - -#breadcrumbs { + margin: 0; + display: inline-block; + float: left; + position: relative; + top: 2px; +} +#kb-search #breadcrumbs { color: #333; margin-bottom: 15px; } -#breadcrumbs a { +#kb-search #breadcrumbs #breadcrumbs a { color: #555; } - -/* New Ticket & Log In Forms */ -#ticketForm div, #clientLogin div { +#ticketForm div, +#clientLogin div { clear: both; padding: 3px 0; overflow: hidden; } -#ticketForm div label, #clientLogin div label { +#ticketForm div label, +#clientLogin div label { display: block; width: 140px; float: left; } -#ticketForm div input, #ticketForm div textarea, #clientLogin div input, #clientLogin div textarea { +#ticketForm div label.required, +#clientLogin div label.required { + font-weight: bold; + text-align: left; +} +#ticketForm div input, +#clientLogin div input, +#ticketForm div textarea, +#clientLogin div textarea { width: auto; border: 1px solid #aaa; background: #fff; @@ -518,23 +548,40 @@ body { display: block; float: left; } - -#ticketForm div input[type=file] { +#ticketForm div input[type=file], +#clientLogin div input[type=file] { border: 0; } - -#ticketForm div select, #clientLogin div select { +#ticketForm div select, +#clientLogin div select { display: block; float: left; } -#ticketForm td textarea, #clientLogin div textarea { +#ticketForm div div.captchaRow, +#clientLogin div div.captchaRow { + line-height: 31px; +} +#ticketForm div div.captchaRow input, +#clientLogin div div.captchaRow input { + position: relative; + top: 6px; +} +#ticketForm td textarea, +#clientLogin td textarea, +#ticketForm div textarea, +#clientLogin div textarea { width: 600px; } - -#ticketForm td em, #clientLogin div em { +#ticketForm td em, +#clientLogin td em, +#ticketForm div em, +#clientLogin div em { color: #777; } -#ticketForm td .captcha, #clientLogin div .captcha { +#ticketForm td .captcha, +#clientLogin td .captcha, +#ticketForm div .captcha, +#clientLogin div .captcha { width: 88px; height: 31px; background: #000; @@ -542,44 +589,31 @@ body { float: left; margin-right: 20px; } -#ticketForm td label.inline, #clientLogin div label.inline { +#ticketForm td label.inline, +#clientLogin td label.inline, +#ticketForm div label.inline, +#clientLogin div label.inline { width: auto; padding: 0 10px; } - -#ticketTable table tr th { - width: 160px; +#ticketForm div.error input, +#clientLogin div.error input { + border: 1px solid #a00; +} +#ticketForm div.error label, +#clientLogin div.error label { + color: #a00; +} +#ticketTable th { + padding-left: 3px; font-weight: normal; text-align: left; } - -#ticketForm table th.required, #ticketForm table td.required, #clientLogin div label.required { +#ticketTable th.required, +#ticketTable td.required { font-weight: bold; text-align: left; } - - - -#ticketForm tr.captchaRow, #clientLogin div.captchaRow { - line-height: 31px; -} - -.captchaRow td 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; -} -#clientLogin p { - clear: both; - text-align: center; -} - #clientLogin { width: 400px; margin-top: 20px; @@ -587,6 +621,10 @@ body { border: 1px solid #ccc; background: url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6; } +#clientLogin p { + clear: both; + text-align: center; +} #clientLogin strong { font-size: 11px; color: #d00; @@ -601,59 +639,89 @@ body { width: 120px; margin-right: 0; } - -/* Ticket List */ +#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; +} +.uploads { + display: inline-block; + padding-right: 20px; +} +.uploads label { + padding: 3px; + padding-right: 10px; + width: auto !important; +} +/* Ticket icons */ .Icon { width: auto; padding-left: 20px; - background-position: left center; + background-position: top left; 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; + background-image: url('../images/icons/ticket.gif'); } - .Icon.webTicket { - background: url('../images/icons/ticket_source_web.gif?1319654283') 0 0 no-repeat; + background-image: url('../images/icons/ticket_source_web.gif'); } - .Icon.emailTicket { - background: url('../images/icons/ticket_source_email.gif?1319654484') 0 0 no-repeat; + background-image: url('../images/icons/ticket_source_email.gif'); } - .Icon.phoneTicket { - background: url('../images/icons/ticket_source_phone.gif?1319654401') 0 0 no-repeat; + background-image: url('../images/icons/ticket_source_phone.gif'); } - .Icon.otherTicket { - background: url('../images/icons/ticket_source_other.gif?1319654433') 0 0 no-repeat; + background-image: url('../images/icons/ticket_source_other.gif'); } - .Icon.attachment { - background-image: url('../images/icons/attachment.gif?1319556657'); + background-image: url('../images/icons/attachment.gif'); } - .Icon.file { - background-image: url('../images/icons/attachment.gif?1319556657'); + background-image: url('../images/icons/attachment.gif'); } - .Icon.refresh { - background-image: url('../images/icons/refresh.gif?1319556657'); + background-image: url('../images/icons/refresh.gif'); } - .Icon.thread { font-weight: bold; font-size: 1em; background-image: url('../images/icons/thread.gif?1319556657'); } - +.Icon:hover { + text-decoration: underline; +} #ticketTable { border: 1px solid #aaa; border-left: none; @@ -688,13 +756,11 @@ a.Icon:hover { #ticketTable tr.alt td { background: #f9f9f9; } - #ticketSearchForm { display: inline-block; float: left; padding: 0 0 5px 0; } - a.refresh { display: block; width: auto; @@ -708,16 +774,14 @@ a.refresh { color: #333; background-position: 5px 50%; background-repeat: no-repeat; - background-image: url('../images/icons/refresh.png?1319653435'); + background-image: url('../images/icons/refresh.png'); } - .infoTable { background: #F4FAFF; } .infoTable th { text-align: left; } - #ticketThread table { margin-top: 10px; border: 1px solid #aaa; @@ -729,13 +793,11 @@ a.refresh { font-size: 12px; padding: 5px; } - #ticketThread table th span { - font-weight:normal; - color:#888; - padding-left:20px; + font-weight: normal; + color: #888; + padding-left: 20px; } - #ticketThread table td { padding: 5px; } @@ -761,76 +823,3 @@ a.refresh { background-position: 0 50%; background-repeat: no-repeat; } - -#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; -} - -.button, .button:visited { - background: #222; - display: inline-block; - font-size: 16px; - padding: 8px 16px 6px 16px; - width:160px; - text-align:center; - color: #fff; - font-weight:bold; - text-decoration: none; - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - box-shadow: 0 1px 3px rgba(0,0,0,0.5); - -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5); - -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5); - text-shadow: 0 -1px 1px rgba(0,0,0,0.25); - border-bottom: 1px solid rgba(0,0,0,0.25); - position: relative; - cursor: pointer; - font-family:helvetica, arial, sans-serif; -} - - -.uploads { - display:inline-block; - padding-right:20px; -} - -.uploads label { padding:3px; padding-right:10px; width: auto !important } - -.button:hover { background-color: #111; color: #fff; } -.button:active { top: 1px; box-shadow:none; -moz-box-shadow:none; -webkit-box-shadow:none; } -.button, .button:visited, -.green.button, .green.button:visited { background-color: #91bd09; } -.green.button:hover { background-color: #749a02; } -.blue.button, .blue.button:visited { background-color: #00AEEF; } -.blue.button:hover { background-color: #0299d2; } - diff --git a/assets/default/css/theme.min.css b/assets/default/css/theme.min.css new file mode 100644 index 0000000000000000000000000000000000000000..8e747a44528fe4376371c967fd867ec835f01fce --- /dev/null +++ b/assets/default/css/theme.min.css @@ -0,0 +1 @@ +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}th,td{vertical-align:top}th{text-align:left;font-weight:normal}h1,h2,h3,h4,h5,h6,form,fieldset{margin:0;padding:0}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}.centered{text-align:center}.clear{clear:both;height:1px;visibility:none}.hidden{display:none}.faded{color:#666}#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 li a{margin-right:2px;display:block;float:left;padding:3px 6px;text-decoration:none}#pagination li 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}#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 #ffd}#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}.button,.button:visited{background:#222;display:inline-block;font-size:16px;padding:8px 16px 6px 16px;width:160px;text-align:center;color:#fff;font-weight:bold;text-decoration:none;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;box-shadow:0 1px 3px rgba(0,0,0,0.5);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.5);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.5);text-shadow:0 -1px 1px rgba(0,0,0,0.25);border-bottom:1px solid rgba(0,0,0,0.25);position:relative;cursor:pointer;font-family:helvetica,arial,sans-serif}.button:hover{background-color:#111;color:#fff}.button:active{top:1px;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none}.button,.button:visited,.green.button,.green.button:visited{background-color:#91bd09}.green.button:hover{background-color:#749a02}.blue.button,.blue.button:visited{background-color:#00aeef}.blue.button:hover{background-color:#0299d2}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;box-shadow:0 3px 2px rgba(0,0,0,0.4);-moz-box-shadow:0 3px 2px rgba(0,0,0,0.4);-webkit-box-shadow:0 3px 2px rgba(0,0,0,0.4)}#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;height:auto!important;height:350px;min-height:350px}#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:0;text-indent:-9999px;margin:0 auto;background:url('../images/poweredby.png') top left no-repeat}#landing_page #new_ticket{margin-top:40px;width:295px;padding-left:75px;float:left;background:url('../images/new_ticket_icon.png') 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') top left no-repeat}#faq{clear:both;margin:0;padding:5px}#faq ol{font-size:15px;margin-left:0;padding-left:0;border-top:1px solid #ddd}#faq ol li{list-style:none;margin:0;padding:0;color:#999}#faq ol li a{display:block;padding:5px 0;height:auto!important;overflow:hidden;margin:0;border-bottom:1px solid #ddd;line-height:16px;padding-left:24px;background:url('../images/icons/page.png') 0 50% no-repeat}#faq ol li a:hover{background-color:#e9f5ff}#faq .article-meta{padding:5px;background:#fafafa}#kb{margin:2px 0;padding:5px;overflow:hidden}#kb>li{padding:10px;height:auto!important;overflow:hidden;margin:0;background:url(../images/kb_category_bg.png) bottom left repeat-x;border-bottom:1px solid #ddd}#kb>li h4{padding-bottom:3px;margin-bottom:3px}#kb>li h4 span{color:#666}#kb>li h4 a{font-size:14px}#kb li i{display:block;width:32px;height:32px;float:left;margin-right:6px;background:url(../images/kb_large_folder.png) top left no-repeat}#kb-search{padding:10px 0;overflow:hidden}#kb-search div{clear:both;overflow:hidden;padding-top:5px}#kb-search #query{margin:0;display:inline-block;float:left;width:200px;margin-right:5px}#kb-search #cid{margin:0;display:inline-block;float:left;width:200px;margin-right:5px;position:relative;top:2px}#kb-search #topic-id{margin:0;display:inline-block;float:left;width:410px}#kb-search #searchSubmit{margin:0;display:inline-block;float:left;position:relative;top:2px}#kb-search #breadcrumbs{color:#333;margin-bottom:15px}#kb-search #breadcrumbs #breadcrumbs a{color:#555}#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 label.required,#clientLogin div label.required{font-weight:bold;text-align:left}#ticketForm div input,#clientLogin div input,#ticketForm div textarea,#clientLogin div textarea{width:auto;border:1px solid #aaa;background:#fff;margin-right:10px;display:block;float:left}#ticketForm div input[type=file],#clientLogin div input[type=file]{border:0}#ticketForm div select,#clientLogin div select{display:block;float:left}#ticketForm div div.captchaRow,#clientLogin div div.captchaRow{line-height:31px}#ticketForm div div.captchaRow input,#clientLogin div div.captchaRow input{position:relative;top:6px}#ticketForm td textarea,#clientLogin td textarea,#ticketForm div textarea,#clientLogin div textarea{width:600px}#ticketForm td em,#clientLogin td em,#ticketForm div em,#clientLogin div em{color:#777}#ticketForm td .captcha,#clientLogin td .captcha,#ticketForm div .captcha,#clientLogin div .captcha{width:88px;height:31px;background:#000;display:block;float:left;margin-right:20px}#ticketForm td label.inline,#clientLogin td label.inline,#ticketForm div label.inline,#clientLogin div label.inline{width:auto;padding:0 10px}#ticketForm div.error input,#clientLogin div.error input{border:1px solid #a00}#ticketForm div.error label,#clientLogin div.error label{color:#a00}#ticketTable th{width:160px;font-weight:normal;text-align:left}#ticketTable th.required,#ticketTable td.required{font-weight:bold;text-align:left}#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 p{clear:both;text-align:center}#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}#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}.uploads{display:inline-block;padding-right:20px}.uploads label{padding:3px;padding-right:10px;width:auto!important}.Icon{width:auto;padding-left:20px;background-position:top left;background-repeat:no-repeat;color:#069;text-decoration:none}.Icon.Ticket{background-image:url('../images/icons/ticket.gif')}.Icon.webTicket{background-image:url('../images/icons/ticket_source_web.gif')}.Icon.emailTicket{background-image:url('../images/icons/ticket_source_email.gif')}.Icon.phoneTicket{background-image:url('../images/icons/ticket_source_phone.gif')}.Icon.otherTicket{background-image:url('../images/icons/ticket_source_other.gif')}.Icon.attachment{background-image:url('../images/icons/attachment.gif')}.Icon.file{background-image:url('../images/icons/attachment.gif')}.Icon.refresh{background-image:url('../images/icons/refresh.gif')}.Icon.thread{font-weight:bold;font-size:1em;background-image:url('../images/icons/thread.gif?1319556657')}.Icon:hover{text-decoration:underline}#ticketTable{border:1px solid #aaa;border-left:none;border-bottom:0}#ticketTable caption{padding:5px;text-align:left;color:#000;background:#ddd;border:1px solid #aaa;border-bottom:0;font-weight:bold}#ticketTable th{height:24px;line-height:24px;background:#e1f2ff;border:1px solid #aaa;border-right:0;border-top:0}#ticketTable th a{color:#000}#ticketTable td{padding:2px;border:1px solid #aaa;border-right:0;border-top:0}#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')}.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:12px;padding:5px}#ticketThread table th span{font-weight:normal;color:#888;padding-left:20px}#ticketThread table td{padding:5px}#ticketThread .message th{background:#d8efff}#ticketThread .response th{background:#ddd}#ticketThread .info{padding:2px;background:#f9f9f9;border-top:1px solid #ddd;height:16px;line-height:16px}#ticketThread .info a{display:inline-block;margin:5px 10px 5px 0;padding-left:24px;height:16px;line-height:16px;background-position:0 50%;background-repeat:no-repeat} \ No newline at end of file diff --git a/assets/default/less/base.less b/assets/default/less/base.less new file mode 100644 index 0000000000000000000000000000000000000000..b6b4681574e7f11178100467eca70ab2ef0fa266 --- /dev/null +++ b/assets/default/less/base.less @@ -0,0 +1,131 @@ +/* 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; + li { + border: 0; + margin: 0; + padding: 0; + font-size: 11px; + list-style: none; + display: inline; + a { + margin-right: 2px; + display: block; + float: left; + padding: 3px 6px; + text-decoration: none; + } + a:hover { + color: #ff0084; + } + } + .previousOff, .nextOff { + color: #666; + display: block; + float: left; + font-weight: bold; + padding: 3px 4px; + } + .next a, .previous a { + font-weight: bold; + } + .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; + strong { + text-transform: uppercase; + color: #a00; + font-style: normal; + } +} + +.error { + color:#f00; + input { + border:1px solid #f00; + } +} + + +.button, .button:visited { + background: #222; + display: inline-block; + font-size: 16px; + padding: 8px 16px 6px 16px; + width:160px; + text-align:center; + color: #fff; + font-weight:bold; + text-decoration: none; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + box-shadow: 0 1px 3px rgba(0,0,0,0.5); + -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5); + -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5); + text-shadow: 0 -1px 1px rgba(0,0,0,0.25); + border-bottom: 1px solid rgba(0,0,0,0.25); + position: relative; + cursor: pointer; + font-family:helvetica, arial, sans-serif; +} + +.button:hover { background-color: #111; color: #fff; } +.button:active { top: 1px; box-shadow:none; -moz-box-shadow:none; -webkit-box-shadow:none; } +.button, .button:visited, +.green.button, .green.button:visited { background-color: #91bd09; } +.green.button:hover { background-color: #749a02; } +.blue.button, .blue.button:visited { background-color: #00AEEF; } +.blue.button:hover { background-color: #0299d2; } diff --git a/assets/default/less/kb.less b/assets/default/less/kb.less new file mode 100644 index 0000000000000000000000000000000000000000..f31faa4d0b99d4decd83dfda5dff3adad24b5bba --- /dev/null +++ b/assets/default/less/kb.less @@ -0,0 +1,88 @@ +#kb { + margin: 2px 0; + padding: 5px; + overflow: hidden; + + > li { + padding:10px; + height:auto !important; + overflow:hidden; + margin:0; + background:url(../images/kb_category_bg.png) bottom left repeat-x; + border-bottom:1px solid #ddd; + h4 { + padding-bottom:3px; + margin-bottom:3px; + span { + color:#666; + } + a { + font-size: 14px; + } + } + } + + li { + i { + display:block; + width:32px; + height:32px; + float:left; + margin-right:6px; + background:url(../images/kb_large_folder.png) top left no-repeat; + } + } +} + +#kb-search { + padding:10px 0; + overflow:hidden; + + div { + clear:both; + overflow:hidden; + padding-top:5px; + } + + #query { + margin:0; + display:inline-block; + float:left; + width:200px; + margin-right:5px; + } + + #cid { + margin:0; + display:inline-block; + float:left; + width:200px; + margin-right:5px; + position:relative; + top:2px; + } + + #topic-id { + margin:0; + display:inline-block; + float:left; + width:410px; + } + + #searchSubmit { + margin:0; + display:inline-block; + float:left; + position:relative; + top:2px; + } + + #breadcrumbs { + color: #333; + margin-bottom: 15px; + + #breadcrumbs a { + color: #555; + } + } +} diff --git a/assets/default/less/landing-page.less b/assets/default/less/landing-page.less new file mode 100644 index 0000000000000000000000000000000000000000..cfcfcfcbf63ff4b7295372dc49b5124043e80d01 --- /dev/null +++ b/assets/default/less/landing-page.less @@ -0,0 +1,58 @@ +#landing_page { + #new_ticket { + margin-top: 40px; + width: 295px; + padding-left: 75px; + float: left; + background: url('../images/new_ticket_icon.png') top left no-repeat; + } + + #check_status { + margin-top: 40px; + width: 295px; + padding-left: 75px; + float: right; + background: url('../images/check_status_icon.png') top left no-repeat; + } +} + +/* Landing page FAQ not yet implemented. */ +#faq { + clear: both; + margin: 0; + padding: 5px; + + ol { + font-size: 15px; + margin-left: 0; + padding-left: 0; + border-top:1px solid #ddd; + + li { + list-style: none; + margin: 0; + padding:0; + color: #999; + a { + display:block; + padding:5px 0; + height:auto !important; + overflow:hidden; + margin:0; + border-bottom:1px solid #ddd; + line-height: 16px; + padding-left: 24px; + background: url('../images/icons/page.png') 0 50% no-repeat; + } + + a:hover { + background-color:#e9f5ff; + } + } + } + .article-meta { + padding:5px; + background:#fafafa; + } +} + diff --git a/assets/default/less/main-layout.less b/assets/default/less/main-layout.less new file mode 100644 index 0000000000000000000000000000000000000000..a40cac090aad2fa4f287fbfa8955d0da04f246de --- /dev/null +++ b/assets/default/less/main-layout.less @@ -0,0 +1,114 @@ +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; + + #logo { + width: 220px; + height: 71px; + float: left; + } + + 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; + box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); + -moz-box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); + -webkit-box-shadow:0 3px 2px rgba(0, 0, 0, 0.4); + + li { + margin: 0; + padding: 0; + list-style: none; + display: inline; + 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; + } + + a.active, a:hover { + background-color: #dbefff; + color: #000; + } + + a:hover { + background-color: #ededed; + color: #0054a6; + } + + a.home { background-image: url('../images/icons/home.png'); } + a.kb { background-image: url('../images/icons/kb.png'); } + a.new { background-image: url('../images/icons/new.png'); } + a.status { background-image: url('../images/icons/status.png'); } + a.tickets { background-image: url('../images/icons/tix.png'); } + } +} + +#content { + padding: 20px 0; + margin: 0 20px; + height: auto !important; + height: 350px; + min-height: 350px; +} + +#footer { + text-align: center; + font-size: 11px; + color: #333; + + a { + color: #333; + } + + p { + margin: 10px 0 0 0; + } + + #poweredBy { + display: block; + width: 126px; + height: 23px; + outline: none; + text-indent: -9999px; + margin: 0 auto; + background: url('../images/poweredby.png') top left no-repeat; + } +} \ No newline at end of file diff --git a/assets/default/less/print.less b/assets/default/less/print.less new file mode 100644 index 0000000000000000000000000000000000000000..dbe21bc7e8d2d2f16cbc7f6db5974383903357f7 --- /dev/null +++ b/assets/default/less/print.less @@ -0,0 +1,30 @@ +#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; + + th { + font-size: 12pt; + font-weight: bold; + padding-bottom: 5px; + } +} diff --git a/assets/default/less/reset.less b/assets/default/less/reset.less new file mode 100644 index 0000000000000000000000000000000000000000..8cf9581eb2516d2c21e5b7b2be0bf614e85d9592 --- /dev/null +++ b/assets/default/less/reset.less @@ -0,0 +1,117 @@ +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; +} + +th, td { + vertical-align: top; +} + +th { text-align: left; font-weight: normal; } + +h1, h2, h3, h4, h5, h6, form, fieldset { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/assets/default/less/theme.less b/assets/default/less/theme.less new file mode 100644 index 0000000000000000000000000000000000000000..c3835f9e608d91533abb782a560b5f4c868cd675 --- /dev/null +++ b/assets/default/less/theme.less @@ -0,0 +1,7 @@ +@import 'reset'; +@import 'base'; +@import 'main-layout'; +@import 'landing-page'; +@import 'kb'; +@import 'ticket-forms'; +@import 'ticket'; diff --git a/assets/default/less/ticket-forms.less b/assets/default/less/ticket-forms.less new file mode 100644 index 0000000000000000000000000000000000000000..96db30b28a692d7db08e3389e2b35317f60b9859 --- /dev/null +++ b/assets/default/less/ticket-forms.less @@ -0,0 +1,170 @@ +#ticketForm, #clientLogin { + div { + clear: both; + padding: 3px 0; + overflow: hidden; + + label { + display: block; + width: 140px; + float: left; + } + + label.required { + font-weight: bold; + text-align: left; + } + + input, textarea { + width: auto; + border: 1px solid #aaa; + background: #fff; + margin-right: 10px; + display: block; + float: left; + } + + input[type=file] { + border: 0; + } + + select { + display: block; + float: left; + } + div.captchaRow { + line-height: 31px; + + input { + position: relative; + top: 6px; + } + } + + } + + td, div { + textarea { + width: 600px; + } + + em { + color: #777; + } + + .captcha { + width: 88px; + height: 31px; + background: #000; + display: block; + float: left; + margin-right: 20px; + } + + label.inline { + width: auto; + padding: 0 10px; + } + } + + div.error { + input { + border: 1px solid #a00; + } + label { + color: #a00; + } + } +} + +#ticketTable { + th { + width: 160px; + font-weight: normal; + text-align: left; + } + th.required, td.required { + font-weight: bold; + text-align: left; + } +} + +#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; + + p { + clear: both; + text-align: center; + } + + strong { + font-size: 11px; + color: #d00; + display: block; + padding-left: 140px; + } + + #email { + width: 250px; + margin-right: 0; + } + + #ticketno { + width: 120px; + margin-right: 0; + } +} + +#reply { + margin-top: 20px; + padding: 10px 5px; + background: #f9f9f9; + border: 1px solid #ccc; + + h2 { + margin-bottom: 10px; + } + + table { + width: 800px; + + td { + vertical-align: top; + } + } + + textarea { + width: 628px !important; + } + + 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; + } +} + +.uploads { + display:inline-block; + padding-right:20px; + + label { + padding:3px; + padding-right:10px; + width: auto !important + } +} \ No newline at end of file diff --git a/assets/default/less/ticket.less b/assets/default/less/ticket.less new file mode 100644 index 0000000000000000000000000000000000000000..d7f2d307930432c2b41e3396f716d77fca5bc3e9 --- /dev/null +++ b/assets/default/less/ticket.less @@ -0,0 +1,145 @@ +/* Ticket icons */ +.Icon { + width: auto; + padding-left: 20px; + background-position: top left; + background-repeat: no-repeat; + color: #006699; + text-decoration: none; +} + +.Icon.Ticket { background-image: url('../images/icons/ticket.gif') } +.Icon.webTicket { background-image: url('../images/icons/ticket_source_web.gif'); } +.Icon.emailTicket { background-image: url('../images/icons/ticket_source_email.gif'); } +.Icon.phoneTicket { background-image: url('../images/icons/ticket_source_phone.gif'); } +.Icon.otherTicket { background-image: url('../images/icons/ticket_source_other.gif'); } +.Icon.attachment { background-image: url('../images/icons/attachment.gif'); } +.Icon.file { background-image: url('../images/icons/attachment.gif'); } +.Icon.refresh { background-image: url('../images/icons/refresh.gif'); } + +.Icon.thread { + font-weight: bold; + font-size: 1em; + background-image: url('../images/icons/thread.gif?1319556657'); +} + +.Icon:hover { + text-decoration: underline; +} + + +#ticketTable { + border: 1px solid #aaa; + border-left: none; + border-bottom: none; + + caption { + padding: 5px; + text-align: left; + color: #000; + background: #ddd; + border: 1px solid #aaa; + border-bottom: none; + font-weight: bold; + } + + th { + height: 24px; + line-height: 24px; + background: #e1f2ff; + border: 1px solid #aaa; + border-right: none; + border-top: none; + + a { + color: #000; + } + } + + td { + padding: 2px; + border: 1px solid #aaa; + border-right: none; + border-top: none; + } + + 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'); +} + +.infoTable { + background: #F4FAFF; + th { + text-align: left; + } +} + +#ticketThread { + table { + margin-top: 10px; + border: 1px solid #aaa; + border-bottom: 2px solid #aaa; + + th { + text-align: left; + border-bottom: 1px solid #aaa; + font-size: 12px; + padding: 5px; + + span { + font-weight:normal; + color:#888; + padding-left:20px; + } + } + + td { + padding: 5px; + } + } + + .message th { background: #d8efff; } + .response th { background: #ddd; } + + .info { + padding: 2px; + background: #f9f9f9; + border-top: 1px solid #ddd; + height: 16px; + line-height: 16px; + + a { + display: inline-block; + margin: 5px 10px 5px 0; + padding-left: 24px; + height: 16px; + line-height: 16px; + background-position: 0 50%; + background-repeat: no-repeat; + } + } +} + diff --git a/assets/font/fontawesome-webfont.eot b/assets/font/fontawesome-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..89070c1e63c2703b2334023922ecc1664f759b55 Binary files /dev/null and b/assets/font/fontawesome-webfont.eot differ diff --git a/assets/font/fontawesome-webfont.svg b/assets/font/fontawesome-webfont.svg new file mode 100755 index 0000000000000000000000000000000000000000..1245f92c2ed23560ac66e37c5e1382e70495e2b9 --- /dev/null +++ b/assets/font/fontawesome-webfont.svg @@ -0,0 +1,255 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="FontAwesomeRegular" horiz-adv-x="1843" > +<font-face units-per-em="2048" ascent="1536" descent="-512" /> +<missing-glyph horiz-adv-x="512" /> +<glyph horiz-adv-x="0" /> +<glyph horiz-adv-x="0" /> +<glyph unicode="
" horiz-adv-x="512" /> +<glyph unicode=" " horiz-adv-x="512" /> +<glyph unicode="	" horiz-adv-x="512" /> +<glyph unicode=" " horiz-adv-x="512" /> +<glyph unicode="o" horiz-adv-x="1591" /> +<glyph unicode="¨" horiz-adv-x="2048" /> +<glyph unicode="©" horiz-adv-x="2048" /> +<glyph unicode="®" horiz-adv-x="2048" /> +<glyph unicode="´" horiz-adv-x="2048" /> +<glyph unicode="Æ" horiz-adv-x="2048" /> +<glyph unicode="Í" horiz-adv-x="2048" /> +<glyph unicode=" " horiz-adv-x="784" /> +<glyph unicode=" " horiz-adv-x="1569" /> +<glyph unicode=" " horiz-adv-x="784" /> +<glyph unicode=" " horiz-adv-x="1569" /> +<glyph unicode=" " horiz-adv-x="523" /> +<glyph unicode=" " horiz-adv-x="392" /> +<glyph unicode=" " horiz-adv-x="261" /> +<glyph unicode=" " horiz-adv-x="261" /> +<glyph unicode=" " horiz-adv-x="196" /> +<glyph unicode=" " horiz-adv-x="313" /> +<glyph unicode=" " horiz-adv-x="87" /> +<glyph unicode=" " horiz-adv-x="313" /> +<glyph unicode="›" horiz-adv-x="2048" /> +<glyph unicode=" " horiz-adv-x="392" /> +<glyph unicode="™" horiz-adv-x="2048" /> +<glyph unicode="∞" horiz-adv-x="2048" /> +<glyph unicode="" horiz-adv-x="1024" d="M0 0z" /> +<glyph unicode="" horiz-adv-x="1536" d="M6 1489q20 47 70 47h1382q51 0 72 -47q20 -47 -17 -84l-610 -610v-641h248q33 0 55.5 -22.5t22.5 -53.5q0 -33 -22.5 -55.5t-55.5 -22.5h-768q-31 0 -53.5 22.5t-22.5 55.5q0 31 22.5 53.5t53.5 22.5h250v641l-610 610q-37 37 -17 84z" /> +<glyph unicode="" horiz-adv-x="1488" d="M0 213q0 57 27.5 103t72.5 77t98.5 47.5t106.5 16.5q25 0 50.5 -4t50.5 -11v779q0 27 16 48t43 29q23 6 99.5 29t178 52.5t215 62.5t211 60.5t164 46t74.5 18.5q35 0 58.5 -23.5t23.5 -58.5v-1028q0 -59 -27.5 -104.5t-73 -76t-99.5 -47t-105 -16.5t-105.5 16.5t-98.5 47 t-71.5 75.5t-27.5 105q0 57 27.5 103t71.5 77t98.5 47t105.5 16q27 0 52.5 -4t49.5 -10v537l-678 -195v-815q0 -59 -27.5 -104.5t-71.5 -76t-98.5 -47t-105.5 -16.5q-53 0 -106.5 16.5t-98.5 47t-72.5 76t-27.5 104.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 901q0 137 52 258t143.5 212t212 143.5t258.5 52.5q137 0 257.5 -52.5t212 -143.5t143.5 -212t52 -258q0 -98 -28.5 -191.5t-81.5 -174.5l358 -359q18 -18 18 -47q0 -16 -18 -43t-45 -53.5t-53.5 -45t-42.5 -18.5q-29 0 -47 19l-359 358q-82 -53 -175 -81.5t-191 -28.5 q-137 0 -258 52t-212.5 143t-143.5 212t-52 258zM266 901q0 -84 32 -156.5t86 -126t127 -85t155 -31.5t154.5 31.5t126.5 85t86 126t32 156.5q0 82 -32 154.5t-86 127t-126.5 86t-154.5 31.5t-155 -31.5t-127 -86t-86 -127t-32 -154.5zM414 901q0 51 19.5 97t54 81t80 54.5 t98.5 19.5q20 0 34.5 -14.5t14.5 -36.5q0 -20 -14.5 -34.5t-34.5 -14.5q-63 0 -107.5 -44t-44.5 -108q0 -20 -14.5 -34.5t-34.5 -14.5q-23 0 -37 14.5t-14 34.5z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 154h1536v852q-31 -31 -58 -50q-106 -80 -212.5 -159.5t-211.5 -163.5q-61 -49 -131.5 -94t-156.5 -45q-82 0 -153 45t-132 94 q-104 84 -211 164t-213 159q-27 18 -57 50v-852zM154 1317q0 -31 14 -65.5t35.5 -66.5t47 -59.5t50.5 -46.5q100 -76 199.5 -150.5t199.5 -152.5q20 -16 48 -37.5t58 -42t59.5 -35t54.5 -14.5h2h2q25 0 54.5 14.5t59 35t57 42t48.5 37.5q100 78 199.5 153t199.5 150 q25 18 50.5 46t47 60t36 66.5t14.5 65.5v65h-1536v-65z" /> +<glyph unicode="" horiz-adv-x="1802" d="M0 1073q0 113 34 205t97.5 155.5t153.5 98.5t202 35q59 0 117 -18.5t110 -48.5t99 -68.5t88 -77.5q39 39 87 77.5t100.5 68.5t109 48.5t115.5 18.5q113 0 204 -35t154.5 -98.5t97 -155.5t33.5 -205q0 -66 -18.5 -130t-51 -124.5t-74.5 -115t-87 -99.5l-615 -612 q-23 -23 -55 -23q-31 0 -57 23l-615 614q-45 45 -87 99.5t-73.5 114t-50 124t-18.5 129.5z" /> +<glyph unicode="" horiz-adv-x="1675" d="M1 959.5q9 27.5 54 33.5l506 74l227 459q20 41 49 41t50 -41l227 -459l506 -74q45 -6 54 -33.5t-23 -60.5l-367 -356l86 -504q8 -45 -15.5 -62.5t-64.5 5.5l-452 237l-453 -237q-41 -23 -64.5 -5.5t-15.5 62.5l86 504l-364 356q-35 33 -26 60.5z" /> +<glyph unicode="" horiz-adv-x="1675" d="M0 948q0 23 18.5 32t36.5 13l506 74l227 459q6 14 20 27.5t30 13.5q18 0 30.5 -13.5t18.5 -27.5l227 -459l506 -74q18 -4 36.5 -13t18.5 -32q0 -14 -7 -26.5t-17 -22.5l-367 -356l86 -504q0 -4 1 -9t1 -12q0 -20 -9 -34.5t-32 -14.5t-41 13l-452 237l-453 -237 q-18 -12 -39 -13q-23 0 -33 14.5t-10 34.5q0 6 1 11.5t1 9.5l86 504l-364 356q-10 10 -18.5 22.5t-8.5 26.5zM289 866l274 -268l-65 -377l340 178l340 -178l-66 377l274 268l-378 56l-170 344l-170 -344z" /> +<glyph unicode="" horiz-adv-x="1566" d="M0 57v387q0 37 18.5 82t48 86t65.5 74t71 43q18 6 66 13.5t102.5 14.5t104.5 13t77 10q-92 59 -144.5 153.5t-52.5 205.5q0 88 34 165.5t91.5 136t135 92.5t165.5 34t166 -34t136.5 -92.5t92 -136t33.5 -165.5q0 -109 -52 -204.5t-144 -154.5q27 -4 77 -10t104 -13 t101 -14.5t68 -13.5q35 -10 70.5 -42t65.5 -74t48.5 -87t18.5 -82v-387q-10 -4 -22.5 -14t-27 -19.5t-27.5 -16.5t-22 -7h-1370q-35 0 -53 21.5t-45 35.5z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM115 154q0 -16 11 -27.5t28 -11.5h153q16 0 27.5 11t11.5 28v153q0 16 -11 27.5t-28 11.5h-153q-16 0 -27.5 -11t-11.5 -28v-153zM115 512 q0 -16 11 -27.5t28 -11.5h153q16 0 27.5 11.5t11.5 27.5v154q0 16 -11 27.5t-28 11.5h-153q-16 0 -27.5 -11.5t-11.5 -27.5v-154zM115 870q0 -16 11 -27.5t28 -11.5h153q16 0 27.5 11.5t11.5 27.5v154q0 16 -11 27.5t-28 11.5h-153q-16 0 -27.5 -11.5t-11.5 -27.5v-154z M115 1229q0 -16 11 -27.5t28 -11.5h153q16 0 27.5 11t11.5 28v153q0 16 -11 27.5t-28 11.5h-153q-16 0 -27.5 -11t-11.5 -28v-153zM461 154q0 -16 11 -27.5t28 -11.5h843q16 0 27.5 11t11.5 28v512q0 16 -11 27.5t-28 11.5h-843q-16 0 -27.5 -11.5t-11.5 -27.5v-512z M461 870q0 -16 11 -27.5t28 -11.5h843q16 0 27.5 11.5t11.5 27.5v512q0 16 -11 27.5t-28 11.5h-843q-16 0 -27.5 -11t-11.5 -28v-512zM1497 154q0 -16 11.5 -27.5t27.5 -11.5h154q16 0 27.5 11t11.5 28v153q0 16 -11.5 27.5t-27.5 11.5h-154q-16 0 -27.5 -11t-11.5 -28v-153 zM1497 512q0 -16 11.5 -27.5t27.5 -11.5h154q16 0 27.5 11.5t11.5 27.5v154q0 16 -11.5 27.5t-27.5 11.5h-154q-16 0 -27.5 -11.5t-11.5 -27.5v-154zM1497 870q0 -16 11.5 -27.5t27.5 -11.5h154q16 0 27.5 11.5t11.5 27.5v154q0 16 -11.5 27.5t-27.5 11.5h-154 q-16 0 -27.5 -11.5t-11.5 -27.5v-154zM1497 1229q0 -16 11.5 -27.5t27.5 -11.5h154q16 0 27.5 11t11.5 28v153q0 16 -11.5 27.5t-27.5 11.5h-154q-16 0 -27.5 -11t-11.5 -28v-153z" /> +<glyph unicode="" d="M0 78v536q0 33 22.5 55.5t55.5 22.5h690q33 0 55.5 -22.5t22.5 -55.5v-536q0 -33 -22.5 -55.5t-55.5 -22.5h-690q-33 0 -55.5 22.5t-22.5 55.5zM0 922v538q0 31 22.5 53.5t55.5 22.5h690q33 0 55.5 -22.5t22.5 -53.5v-538q0 -33 -22.5 -54.5t-55.5 -21.5h-690 q-33 0 -55.5 21.5t-22.5 54.5zM999 78v536q0 33 21.5 55.5t54.5 22.5h692q31 0 53.5 -22.5t22.5 -55.5v-536q0 -33 -22.5 -55.5t-53.5 -22.5h-692q-33 0 -54.5 22.5t-21.5 55.5zM999 922v538q0 31 21.5 53.5t54.5 22.5h692q31 0 53.5 -22.5t22.5 -53.5v-538 q0 -33 -22.5 -54.5t-53.5 -21.5h-692q-33 0 -54.5 21.5t-21.5 54.5z" /> +<glyph unicode="" d="M0 78v270q0 33 22.5 54.5t55.5 21.5h358q31 0 53.5 -21.5t22.5 -54.5v-270q0 -33 -22.5 -55.5t-53.5 -22.5h-358q-33 0 -55.5 22.5t-22.5 55.5zM0 655v226q0 33 22.5 54t55.5 21h358q31 0 53.5 -21.5t22.5 -53.5v-226q0 -33 -22.5 -55t-53.5 -22h-358q-33 0 -55.5 22.5 t-22.5 54.5zM0 1188v272q0 31 22.5 53.5t55.5 22.5h358q31 0 53.5 -22.5t22.5 -53.5v-272q0 -33 -22.5 -55.5t-53.5 -22.5h-358q-33 0 -55.5 22.5t-22.5 55.5zM666 78v270q0 33 22.5 54.5t54.5 21.5h359q31 0 53.5 -21.5t22.5 -54.5v-270q0 -33 -22.5 -55.5t-53.5 -22.5 h-359q-33 0 -55 22.5t-22 55.5zM666 655v226q0 33 22.5 54t54.5 21h359q31 0 53.5 -21.5t22.5 -53.5v-226q0 -33 -22.5 -55t-53.5 -22h-359q-33 0 -55 22.5t-22 54.5zM666 1188v272q0 31 22.5 53.5t54.5 22.5h359q31 0 53.5 -22.5t22.5 -53.5v-272q0 -33 -22.5 -55.5 t-53.5 -22.5h-359q-33 0 -55 22.5t-22 55.5zM1331 78v270q0 33 22.5 54.5t55.5 21.5h358q31 0 53.5 -21.5t22.5 -54.5v-270q0 -33 -22.5 -55.5t-53.5 -22.5h-358q-33 0 -55.5 22.5t-22.5 55.5zM1331 655v226q0 33 22.5 54t55.5 21h358q31 0 53.5 -21.5t22.5 -53.5v-226 q0 -33 -22.5 -55t-53.5 -22h-358q-33 0 -55.5 22.5t-22.5 54.5zM1331 1188v272q0 31 22.5 53.5t55.5 22.5h358q31 0 53.5 -22.5t22.5 -53.5v-272q0 -33 -22.5 -55.5t-53.5 -22.5h-358q-33 0 -55.5 22.5t-22.5 55.5z" /> +<glyph unicode="" d="M0 78v270q0 33 22.5 54.5t55.5 21.5h297q31 0 53.5 -21.5t22.5 -54.5v-270q0 -33 -22.5 -55.5t-53.5 -22.5h-297q-33 0 -55.5 22.5t-22.5 55.5zM0 655v226q0 33 22.5 54t55.5 21h297q31 0 53.5 -21.5t22.5 -53.5v-226q0 -33 -22.5 -55t-53.5 -22h-297q-33 0 -55.5 22.5 t-22.5 54.5zM0 1188v272q0 31 22.5 53.5t55.5 22.5h297q31 0 53.5 -22.5t22.5 -53.5v-272q0 -33 -22.5 -55.5t-53.5 -22.5h-297q-33 0 -55.5 22.5t-22.5 55.5zM604 78v270q0 33 22.5 54.5t55.5 21.5h1085q31 0 53.5 -21.5t22.5 -54.5v-270q0 -33 -22.5 -55.5t-53.5 -22.5 h-1085q-33 0 -55.5 22.5t-22.5 55.5zM604 655v226q0 33 22.5 54t55.5 21h1085q31 0 53.5 -21.5t22.5 -53.5v-226q0 -33 -22.5 -55t-53.5 -22h-1085q-33 0 -55.5 22.5t-22.5 54.5zM604 1188v272q0 31 22.5 53.5t55.5 22.5h1085q31 0 53.5 -22.5t22.5 -53.5v-272 q0 -33 -22.5 -55.5t-53.5 -22.5h-1085q-33 0 -55.5 22.5t-22.5 55.5z" /> +<glyph unicode="" d="M0 732.5q0 33.5 23 55.5l174 175q23 23 56.5 22.5t55.5 -22.5l365 -365q23 -23 56.5 -23t55.5 23l746 745q23 23 56.5 23t56.5 -23l174 -174q23 -23 22.5 -56.5t-22.5 -55.5l-910 -910q-23 -23 -62.5 -39t-72.5 -16h-88q-35 0 -75 16.5t-62 38.5l-526 529 q-23 23 -23 56.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 192.5q0 38.5 29 67.5l508 510l-508 500q-29 29 -29 67.5t29 67.5l100 100q29 29 68 29t67 -29l504 -504l504 504q29 29 67.5 29t67.5 -29l100 -100q29 -29 29 -68t-29 -67l-508 -510l508 -500q29 -29 29 -66.5t-29 -66.5l-100 -102q-29 -29 -68 -29t-67 29l-504 505 l-506 -505q-29 -29 -66.5 -29t-66.5 29l-100 100q-29 29 -29 67.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 901q0 137 52 258t143.5 212t212 143.5t258.5 52.5q137 0 259 -52.5t212 -143.5t142 -212t52 -258q0 -102 -28.5 -195.5t-81.5 -170.5l358 -359q18 -18 18 -46t-18 -48l-94 -94q-20 -18 -48 -18.5t-46 18.5l-359 358q-78 -53 -171 -81.5t-195 -28.5q-137 0 -258 52 t-212.5 142t-143.5 211t-52 260zM266 901q0 -84 32 -156.5t86 -126t127 -85t155 -31.5t155.5 31.5t126.5 85t85 126t32 156.5q0 82 -32 154.5t-85 127t-126.5 86t-155.5 31.5t-155 -31.5t-127 -86t-86 -127t-32 -154.5zM399 868v66q0 33 33 33h168v168q0 33 33 32h65 q14 0 24.5 -9t10.5 -23v-168h166q33 0 33 -33v-66q0 -14 -9.5 -24t-23.5 -10h-166v-166q0 -14 -10 -23.5t-25 -9.5h-65q-33 0 -33 33v166h-168q-14 0 -23.5 10t-9.5 24z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 901q0 137 52 258t143.5 212t212 143.5t258.5 52.5q137 0 259 -52.5t212 -143.5t142 -212t52 -258q0 -102 -28.5 -195.5t-81.5 -170.5l358 -359q18 -18 18 -46t-18 -48l-94 -94q-20 -18 -48 -18.5t-46 18.5l-359 358q-78 -53 -171 -81.5t-195 -28.5q-137 0 -258 52 t-212.5 142t-143.5 211t-52 260zM266 901q0 -84 32 -156.5t86 -126t127 -85t155 -31.5t155.5 31.5t126.5 85t85 126t32 156.5q0 82 -32 154.5t-85 127t-126.5 86t-155.5 31.5t-155 -31.5t-127 -86t-86 -127t-32 -154.5zM399 868v66q0 33 33 33h467q33 0 33 -33v-66 q0 -14 -9.5 -24t-23.5 -10h-467q-14 0 -23.5 10t-9.5 24z" /> +<glyph unicode="" horiz-adv-x="1488" d="M0 713q0 186 86 349t240 267q12 10 28 6q18 -4 25 -16l90 -131q10 -12 6 -27.5t-16 -25.5q-106 -72 -164.5 -182.5t-58.5 -239.5q0 -104 39.5 -197.5t108.5 -162t162 -108.5t197 -40t197.5 40t163.5 108.5t109.5 161.5t39.5 198q0 129 -59 239.5t-164 182.5 q-14 10 -16 24q-4 16 6 29l88 131q10 12 25.5 15t29.5 -5q154 -104 240 -267t86 -349q0 -154 -58.5 -289t-160 -236.5t-237.5 -160t-290 -58.5t-289 58.5t-236 160t-159.5 236.5t-58.5 289zM627 793v704q0 16 11 27.5t28 11.5h157q16 0 27.5 -11.5t11.5 -27.5v-704 q0 -16 -11 -27.5t-28 -11.5h-157q-16 0 -27.5 11t-11.5 28z" /> +<glyph unicode="" d="M0 39v260q0 16 11.5 27.5t27.5 11.5h192q16 0 26.5 -11.5t10.5 -27.5v-260q0 -16 -10 -27.5t-27 -11.5h-192q-39 0 -39 39zM393 39v434q0 16 11.5 27.5t27.5 11.5h193q16 0 26.5 -11.5t10.5 -27.5v-434q0 -16 -10.5 -27.5t-26.5 -11.5h-193q-16 0 -27.5 11.5t-11.5 27.5z M786 39v676q0 16 11.5 27.5t27.5 11.5h193q16 0 27.5 -11.5t11.5 -27.5v-676q0 -16 -11.5 -27.5t-27.5 -11.5h-193q-16 0 -27.5 11.5t-11.5 27.5zM1182 39v995q0 16 10 27.5t27 11.5h192q16 0 27.5 -11t11.5 -28v-995q0 -16 -11.5 -27.5t-27.5 -11.5h-192q-16 0 -26.5 11.5 t-10.5 27.5zM1575 39v1458q0 39 39 39h190q39 0 39 -39v-1458q0 -39 -39 -39h-190q-39 0 -39 39z" /> +<glyph unicode="" horiz-adv-x="1593" d="M0 651v236q0 12 30.5 21.5t68.5 15.5t74 9t48 5q18 61 49 117q-55 82 -120 157l-7 15q0 8 28 38.5t62.5 65.5t66.5 62.5t40 27.5q2 0 26.5 -18.5t54.5 -41t56.5 -43t32.5 -24.5q29 16 58.5 26.5t60.5 20.5q0 12 3 49t9 75t15.5 69t21.5 31h237q14 0 19 -15 q12 -49 17 -103t14 -106q31 -8 59.5 -19t56.5 -28q8 6 34 26.5t55.5 43t53 40t29.5 17.5t37 -27.5t65 -62.5t61.5 -65.5t27.5 -38.5q0 -4 -17.5 -28.5t-39 -53.5t-42 -55.5t-24.5 -32.5q33 -55 51 -123q49 -10 103.5 -13t101.5 -20q16 -4 16 -18v-236q0 -12 -29.5 -21.5 t-68.5 -15.5t-76 -9t-49 -5q-14 -57 -47 -117q55 -82 121 -157l6 -15q0 -8 -27.5 -38.5t-62.5 -65.5t-66.5 -62.5t-40.5 -27.5q-2 0 -26.5 18.5t-54 41t-56 43t-33.5 24.5q-29 -16 -58.5 -27.5t-59.5 -19.5q-2 -12 -5.5 -49.5t-9.5 -76t-14 -69.5t-21 -31h-237q-14 0 -19 17 q-14 49 -19 103t-11 103q-61 18 -117 50q-41 -31 -81 -60.5t-79 -62.5l-12 -4q-6 0 -37 27.5t-64.5 62.5t-61 65.5t-27.5 38.5q0 2 16 26.5t37.5 53.5t42 55.5t26.5 34.5q-33 55 -51 123q-51 10 -104.5 13t-100.5 20q-16 4 -16 18zM557 768q0 -49 18.5 -93t51 -77t77 -52.5 t93.5 -19.5t93 19.5t75.5 52.5t51 77t19.5 93t-19.5 92t-51 76t-75.5 51.5t-93 18.5t-93.5 -18.5t-77 -51.5t-51 -76t-18.5 -92z" /> +<glyph unicode="" horiz-adv-x="1304" d="M0 1175.5v34.5v36t2 36q25 14 71 23.5t98 15.5t102.5 9t78.5 5q-8 82 11.5 128t59.5 68.5t98.5 29t131.5 6.5q55 0 109.5 -3t97.5 -20.5t68.5 -54.5t25.5 -105v-24t-2 -25q29 -2 79 -5t102.5 -9t99.5 -15.5t72 -23.5v-72v-69q-37 -20 -123 -32.5t-185.5 -19t-193.5 -7.5 t-150 -1q-55 0 -150 1t-194.5 7.5t-184.5 18.5t-122 33q-2 16 -2 34.5zM133 154v837q123 -16 244 -21t244 -5h32q129 2 258 6t258 20v-837q0 -63 -44 -108.5t-107 -45.5h-731q-63 0 -108.5 45t-45.5 109zM303 199q0 -16 10.5 -26.5t26.5 -10.5h39q16 0 27.5 10t11.5 27v614 q0 16 -11.5 27.5t-27.5 11.5h-39q-16 0 -26.5 -11.5t-10.5 -27.5v-614zM504 1384q0 -10 1 -22t3 -23q144 2 291 0q0 12 2 23.5t0 21.5v15q-35 10 -74 11t-74 1q-37 0 -75.5 -1t-73.5 -11v-15zM596 199q0 -16 10 -26.5t27 -10.5h39q16 0 27.5 10t11.5 27v614q0 16 -11.5 27.5 t-27.5 11.5h-39q-16 0 -26.5 -11.5t-10.5 -27.5v-614zM887 199q0 -16 11 -26.5t28 -10.5h39q16 0 27.5 10t11.5 27v614q0 16 -11.5 27.5t-27.5 11.5h-39q-16 0 -27.5 -11.5t-11.5 -27.5v-614z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 809.5q2 15.5 14 26.5l867 710q27 20 59 21q33 0 59 -21l240 -196v102q0 16 11.5 27.5t27.5 11.5h223q16 0 26.5 -11.5t10.5 -27.5v-348l328 -268q12 -10 14 -25.5t-8 -28.5l-45 -53q-10 -14 -29 -14h-65q-16 0 -25 8l-743 608q-25 20 -50 0l-743 -608q-8 -8 -25 -8 h-65q-18 0 -29 14l-45 53q-10 12 -8 27.5zM266 76v622l674 553l674 -553v-622q0 -33 -21.5 -54.5t-54.5 -21.5h-422v498h-352v-498h-422q-33 0 -54.5 21.5t-21.5 54.5z" /> +<glyph unicode="" horiz-adv-x="1228" d="M0 78v1382q0 31 22.5 53.5t55.5 22.5h614q33 0 71 -16.5t60 -38.5l351 -351q23 -23 39 -60.5t16 -70.5v-921q0 -33 -22.5 -55.5t-53.5 -22.5h-1075q-33 0 -55.5 22.5t-22.5 55.5zM154 154h921v692h-459q-31 0 -53 22.5t-22 55.5v458h-387v-1228zM268 326v116h693v-116 h-693zM268 596v115h693v-115h-693zM694 999h381q0 4 -4 13.5t-6 11.5l-350 348q-2 4 -9.5 6t-11.5 4v-383z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177.5 120t-217.5 44q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM635 608v465q0 33 23.5 56.5t56.5 23.5h80q35 0 57 -23.5t22 -56.5v-305h201q33 0 56.5 -23.5t23.5 -56.5v-80q0 -33 -23.5 -56.5t-56.5 -23.5h-360q-33 0 -56.5 23.5 t-23.5 56.5z" /> +<glyph unicode="" d="M2 35l594 1466q6 14 21.5 24.5t31.5 10.5h195l-8 -170h172l-9 170h195q16 0 31.5 -10t21.5 -25l594 -1466q6 -14 -1 -24.5t-23 -10.5h-740l-26 512h-258l-27 -512h-739q-16 0 -23.5 10t-1.5 25zM807 797h229l-20 413h-189z" /> +<glyph unicode="" d="M0 39v614q0 16 11.5 27.5t27.5 11.5h229q16 0 27.5 -11t11.5 -28v-346h1229v346q0 16 11.5 27.5t27.5 11.5h229q16 0 27.5 -11t11.5 -28v-614q0 -39 -39 -39h-1765q-39 0 -39 39zM345.5 944.5q6.5 16.5 39.5 16.5h307v499q0 31 21.5 53.5t54.5 22.5h307q33 0 55.5 -22.5 t22.5 -53.5v-499h307q31 0 37 -16.5t-16 -39.5l-504 -506q-23 -23 -55.5 -22.5t-55.5 22.5l-504 506q-23 23 -16.5 39.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177.5 120t-217.5 44q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM414 743q10 25 37 25h208v358q0 16 11.5 28.5t27.5 12.5h201q16 0 27.5 -12t11.5 -29v-358h209q27 0 37 -25t-10 -43l-347 -346q-14 -10 -28 -10t-29 10l-346 346 q-20 18 -10 43z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177 120t-218 44t-218.5 -44t-177 -120t-119.5 -177t-44 -218zM414 793q-10 25 10 43l346 346q14 10 29 10q14 0 28 -10l347 -346q20 -18 10 -43t-37 -25h-209v-360q0 -16 -11.5 -27.5t-27.5 -11.5h-201q-16 0 -27.5 11t-11.5 28v360h-208 q-27 0 -37 25z" /> +<glyph unicode="" d="M0 78v577q0 33 9 76t22 72l284 663q12 29 44 49.5t63 20.5h999q31 0 63 -20.5t44 -49.5l284 -663q12 -29 21.5 -72t9.5 -76v-577q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM238 694h387l114 -231h383l117 231h367q-2 4 -2 9.5t-2 9.5l-256 594 h-848l-256 -596q-2 -2 -2 -7.5t-2 -9.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177.5 120t-217.5 44q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM582 453v628q0 18 16 29q20 8 31 0l545 -315q16 -6 16 -27q0 -20 -16 -27l-545 -315q-8 -4 -15 -4q-8 0 -16 4q-16 10 -16 27z" /> +<glyph unicode="" horiz-adv-x="1591" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5q141 0 271 -48.5t239 -140.5l161 162q35 35 58.5 24.5t23.5 -57.5v-463q0 -33 -22 -55q-10 -10 -23.5 -16t-29.5 -6h-463q-47 0 -58.5 23.5t23.5 58.5l160 159q-72 57 -159 88t-181 31q-117 0 -218.5 -44t-177 -120 t-119.5 -177t-44 -218t44 -218t119.5 -177t177 -120t218.5 -44q104 0 198.5 37t169 101.5t123.5 153.5t64 191q0 16 14 27q14 10 31 8l157 -20q16 -4 26.5 -16.5t8.5 -28.5q-20 -147 -89.5 -274.5t-176 -220.5t-242 -145.5t-284.5 -52.5q-166 0 -311.5 62.5t-254 171 t-171 254t-62.5 311.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M6 311l150 437q10 33 39 45t59 4l438 -150q45 -16 46 -40.5t-42 -47.5l-202 -100q53 -78 123.5 -134.5t160.5 -86.5q111 -37 221.5 -28t207 56t170 129t110.5 193q6 16 20.5 22t30.5 2l152 -51q16 -6 23 -20.5t1 -30.5q-53 -158 -159.5 -274.5t-243.5 -184t-296 -81 t-315 39.5q-135 47 -241.5 134t-179.5 208l-205 -100q-43 -23 -63.5 -5.5t-4.5 64.5zM203 1024q53 156 159.5 273.5t243.5 185t295 81t316 -39.5q133 -47 240.5 -134t180.5 -208l205 100q43 23 63.5 5.5t4.5 -62.5l-150 -439q-10 -33 -39 -45t-59 -4l-438 150 q-45 16 -46 40.5t40 47.5l202 100q-51 78 -122.5 134.5t-159.5 86.5q-111 37 -221.5 28t-207 -56t-170 -129t-110.5 -193q-6 -16 -20.5 -22t-30.5 -2l-152 51q-16 6 -23 20.5t-1 30.5z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 154h1536v1075h-1536v-1075zM307 346v76q0 39 39 39h154q16 0 27.5 -11.5t11.5 -27.5v-76q0 -16 -11.5 -27.5t-27.5 -11.5h-154q-39 0 -39 39 zM307 653v76q0 39 39 39h154q16 0 27.5 -11.5t11.5 -27.5v-76q0 -16 -11.5 -27.5t-27.5 -11.5h-154q-39 0 -39 39zM307 961v75q0 39 39 39h154q16 0 27.5 -11t11.5 -28v-75q0 -16 -11.5 -27.5t-27.5 -11.5h-154q-39 0 -39 39zM692 346v76q0 16 10.5 27.5t26.5 11.5h768 q39 0 39 -39v-76q0 -39 -39 -39h-768q-16 0 -26.5 11.5t-10.5 27.5zM692 653v76q0 16 10.5 27.5t26.5 11.5h768q39 0 39 -39v-76q0 -39 -39 -39h-768q-16 0 -26.5 11.5t-10.5 27.5zM692 961v75q0 16 10.5 27.5t26.5 11.5h768q39 0 39 -39v-75q0 -39 -39 -39h-768 q-16 0 -26.5 11t-10.5 28z" /> +<glyph unicode="" horiz-adv-x="1253" d="M0 117v626q0 39 21.5 69t56.5 42v164q0 113 43 213t117.5 175t175 118t213.5 43t213 -43t175 -118t118 -175.5t43 -212.5v-164q35 -12 56 -42t21 -69v-626q0 -47 -34.5 -82t-81.5 -35h-1020q-47 0 -82 35t-35 82zM313 862h627v156q0 66 -24.5 123t-67.5 99t-100.5 66.5 t-120.5 24.5q-66 0 -122 -24.5t-99.5 -66.5t-68 -99.5t-24.5 -122.5v-156zM494 164h266l-66 285q29 18 47.5 48.5t18.5 65.5q0 55 -39 95t-94 40t-94 -40t-39 -95q0 -35 18 -65.5t47 -46.5z" /> +<glyph unicode="" d="M0 1382q0 63 45 108.5t109 45.5q63 0 108 -45.5t45 -108.5q0 -41 -20.5 -74.5t-55.5 -56.5v-1212q0 -16 -11 -27.5t-27 -11.5h-78q-16 0 -26.5 11.5t-10.5 27.5v1212q-35 23 -56.5 56.5t-21.5 74.5zM307 416v745q0 33 19.5 66t48.5 49q104 55 188 86t144 45q70 16 124 18 q68 0 124.5 -11t107.5 -29.5t99 -43t100 -51.5q63 -29 145 -33q70 -4 164 15.5t207 87.5q29 16 47 6t18 -43v-748q0 -31 -18.5 -64.5t-46.5 -49.5q-113 -68 -207 -87.5t-164 -15.5q-82 4 -145 33q-51 27 -99.5 51.5t-99.5 43t-107.5 29.5t-124.5 11q-55 -2 -124 -18 q-59 -14 -143.5 -45t-188.5 -88q-29 -16 -48.5 -4t-19.5 45z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 729q0 172 85 324.5t221 266.5t307 180.5t345 66.5t345.5 -66.5t307.5 -180.5t221 -266.5t85 -326.5q0 -182 -78 -350l-27 -60l-174 -26q-27 -104 -110.5 -173t-196.5 -69v-41q0 -16 -12 -27.5t-29 -11.5h-80q-16 0 -27.5 11.5t-11.5 27.5v719q0 16 11.5 28.5 t27.5 12.5h80q16 0 28.5 -12.5t12.5 -28.5v-39q88 0 161 -44t114 -116l39 7q33 90 32 192q0 125 -65.5 233.5t-170 190.5t-232.5 129t-251 47t-250.5 -47t-232 -128t-170 -190.5t-65.5 -232.5q0 -104 32 -194l39 -7q41 72 114 116t161 44v39q0 16 12 28.5t29 12.5h80 q16 0 27 -12.5t11 -28.5v-719q0 -16 -11 -27.5t-27 -11.5h-80q-16 0 -28.5 11.5t-12.5 27.5v41q-55 0 -106.5 18.5t-91.5 50.5t-68.5 76t-40.5 97l-175 26l-26 60q-78 168 -78 352z" /> +<glyph unicode="" horiz-adv-x="905" d="M0 578v380q0 16 11.5 27.5t27.5 11.5h420l325 326q49 51 84 37t35 -86v-1012q0 -72 -34.5 -86t-84.5 37l-325 326h-420q-16 0 -27.5 11t-11.5 28z" /> +<glyph unicode="" horiz-adv-x="1277" d="M0 578v380q0 16 11.5 27.5t27.5 11.5h420l325 326q49 51 84 37t35 -86v-1012q0 -72 -34.5 -86t-84.5 37l-325 326h-420q-16 0 -27.5 11t-11.5 28zM1027 406q-9 32 7 61q84 145 84 301t-84 301q-16 29 -7 61t38 48t60.5 8t48.5 -37q104 -182 104 -381q0 -201 -104 -381 q-23 -41 -70 -41q-20 0 -39 12q-29 16 -38 48z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 578v380q0 16 11.5 27.5t27.5 11.5h420l325 326q49 51 84 37t35 -86v-1012q0 -72 -34.5 -86t-84.5 37l-325 326h-420q-16 0 -27.5 11t-11.5 28zM1027 406q-9 32 7 61q84 145 84 301t-84 301q-16 29 -7 61t38 48t60.5 8t48.5 -37q104 -182 104 -381q0 -201 -104 -381 q-23 -41 -70 -41q-20 0 -39 12q-29 16 -38 48zM1285 219q-7 33 11 62q141 225 142 487q0 262 -142 487q-18 29 -11 62t36 49q29 18 61 11t50 -36q82 -131 123.5 -275t41.5 -298q0 -309 -167 -573q-10 -18 -29 -27.5t-37 -9.5q-25 0 -43 12q-29 16 -36 49zM1540 33 q-6 33 12 59q100 154 152.5 325t52.5 351t-52 351t-153 323q-18 29 -12 61.5t35 50.5q29 16 61 10.5t50 -32.5q115 -174 173 -366.5t58 -397.5t-58.5 -397.5t-172.5 -364.5q-10 -18 -29 -27.5t-37 -9.5q-25 0 -45 13q-29 18 -35 51z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 0v698h698v-698h-698zM0 838v698h698v-698h-698zM139 139h420v420h-420v-420zM139 977h420v420h-420v-420zM279 279v141h141v-141h-141zM279 1116v139h141v-139h-141zM838 0v698h417v-139h142v139h139v-419h-420v139h-139v-418h-139zM838 838v698h698v-698h-698z M977 977h420v420h-420v-420zM1116 0v139h139v-139h-139zM1116 1116v139h139v-139h-139zM1397 0v139h139v-139h-139z" /> +<glyph unicode="" d="M0 0v1536h154v-1536h-154zM227 0v1536h37v-1536h-37zM356 0v1536h117v-1536h-117zM545 0v1536h78v-1536h-78zM715 0v1536h76v-1536h-76zM903 0v1536h37v-1536h-37zM1014 0v1536h153v-1536h-153zM1221 0v1536h77v-1536h-77zM1409 0v1536h39v-1536h-39zM1579 0v1536h37 v-1536h-37zM1690 0v1536h153v-1536h-153z" /> +<glyph unicode="" horiz-adv-x="1488" d="M0 961v454q0 49 35 85t86 36h454q51 0 113 -24.5t94 -61.5l672 -748q33 -39 34 -88t-34 -84l-526 -526q-35 -35 -86 -36t-86 36l-670 750q-35 37 -60.5 96t-25.5 111zM197 1223q0 -49 33.5 -83t82.5 -34t83 34t34 83t-34 82.5t-83 33.5t-82.5 -33.5t-33.5 -82.5z" /> +<glyph unicode="" horiz-adv-x="1875" d="M0 961v454q0 49 35 85t86 36h454q25 0 53.5 -6t57.5 -18.5t54.5 -28t41.5 -33.5l670 -748q33 -37 34 -86t-34 -84l-526 -524q-35 -35 -86.5 -37t-83.5 37l-670 746q-35 39 -60.5 98t-25.5 109zM195 1223q0 -47 34.5 -82t83.5 -35q47 0 82 35t35 82q0 49 -35 83.5 t-82 34.5q-49 0 -83.5 -34.5t-34.5 -83.5zM791 1534h174q51 0 112.5 -24.5t93.5 -61.5l670 -748q35 -37 35 -87t-35 -85l-524 -524q-35 -35 -86 -36t-86 36l-12 14l514 514q35 35 34.5 84.5t-34.5 85.5l-670 748q-31 35 -84 56.5t-102 27.5z" /> +<glyph unicode="" horiz-adv-x="1710" d="M10 311q2 16 4 31.5t4 34.5q0 10 -4 20.5t-2 20.5q2 16 15.5 31.5t25.5 35.5q23 37 45.5 90.5t32.5 92.5q4 16 -1 30.5t-1 26.5q4 16 16.5 27.5t20.5 23.5q10 18 21.5 42t21.5 49.5t16 50t8 40.5t-2 33t0 29q6 16 20.5 26.5t24.5 24.5q10 12 21.5 34.5t23 49.5t19.5 52.5 t10 45.5q2 12 -4 24.5t-2 27.5q4 14 18.5 29.5t26.5 31.5q16 25 28.5 58.5t30 61t46 43t77.5 1.5l-2 -4q31 10 54 10h780q78 0 119 -57q41 -53 18 -129l-283 -906q-18 -63 -77.5 -107t-126.5 -44h-893q-10 0 -20.5 -2t-18.5 -12q-12 -20 0 -56q16 -43 60 -75.5t87 -32.5h946 q29 0 57.5 21.5t37.5 47.5l309 987q4 16 5 29.5t-1 28.5q41 -14 61 -43q41 -53 19 -129l-283 -905q-18 -66 -77.5 -109t-127.5 -43h-946q-41 0 -79.5 14.5t-73.5 39t-61.5 58t-41.5 72.5q-25 68 -2 127zM500 961q-10 -39 26 -39h615q16 0 30.5 11t18.5 28l24 75 q4 16 -3 27.5t-23 11.5h-615q-16 0 -31.5 -11t-19.5 -28zM569 1190q-4 -16 3.5 -26.5t23.5 -10.5h614q16 0 30.5 10.5t21.5 26.5l22 78q4 16 -3 27.5t-24 11.5h-614q-16 0 -30.5 -11.5t-20.5 -27.5z" /> +<glyph unicode="" horiz-adv-x="1253" d="M0 84v1337q0 47 34 81t81 34h1024q47 0 80.5 -34t33.5 -81v-1337q0 -47 -33.5 -81t-80.5 -34t-80 33l-432 432l-432 -432q-33 -33 -80 -33t-81 34t-34 81z" /> +<glyph unicode="" d="M0 39v346q0 47 18.5 89t50 73t73.5 49t89 18h1383q47 0 89 -18t72.5 -49t49 -73t18.5 -89v-346q0 -39 -39 -39h-1765q-39 0 -39 39zM268 193q0 -16 11.5 -26.5t27.5 -10.5h1229q16 0 27.5 10t11.5 27v38q0 16 -11.5 27.5t-27.5 11.5h-1229q-16 0 -27.5 -11t-11.5 -28v-38 zM307 729v731q0 31 22.5 53.5t55.5 22.5h651v-383q0 -49 34 -83t81 -34h385v-307h-1229zM1151 1153v383l385 -383h-385z" /> +<glyph unicode="" d="M0 115v1075q0 47 34 82t81 35h366l58 125q18 43 66 73.5t95 30.5h443q47 0 95 -30.5t67 -73.5l57 -125h367q47 0 80.5 -35t33.5 -82v-1075q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM442 653q0 -100 38 -187t102.5 -152.5t153 -103.5t186.5 -38t186 38 t152.5 103.5t102.5 152.5t38 187q0 98 -38 186.5t-102.5 153t-152.5 102.5t-186 38t-186.5 -38t-153 -102.5t-102.5 -152.5t-38 -187zM596 653q0 68 25.5 127.5t69.5 103.5t103.5 69.5t127.5 25.5t127 -25.5t103 -69.5t69.5 -103.5t25.5 -127.5t-25.5 -127t-69.5 -104 t-103.5 -70.5t-126.5 -25.5q-68 0 -127.5 25.5t-103.5 70.5t-69.5 104.5t-25.5 126.5z" /> +<glyph unicode="" horiz-adv-x="1644" d="M0 0l2 80q10 4 29.5 8t48.5 8q92 18 108 33q16 10 50 68l233 614l277 725h73h53l11 -20l202 -482q33 -78 64 -151.5t59 -145.5q29 -72 52.5 -130t42.5 -103q12 -29 28.5 -70t36.5 -94q23 -66 64 -150q25 -49 34 -57q20 -18 68 -24q25 -2 49.5 -9.5t52.5 -17.5 q6 -37 7 -55v-10.5t-3 -16.5q-43 0 -90 2t-98 6q-53 4 -99 6t-87 2h-80t-53 -2l-199 -10l-57 -2q0 20 1 39.5t3 38.5l129 26q57 14 67 25q12 8 13 27q0 14 -7 30l-47 115l-90 227l-446 2q-12 -29 -37 -96t-66 -178q-23 -63 -22 -84q0 -27 16 -43q14 -10 40 -17.5t63 -13.5 q14 -4 84 -12v-59q0 -16 -2 -27q-35 0 -121 5t-224 16l-49 -9q-43 -8 -83 -11t-81 -3h-20zM549 655q135 -2 216 -4t105 0l29 2q-18 51 -40.5 111.5t-51.5 130.5t-51.5 122t-38.5 87z" /> +<glyph unicode="" horiz-adv-x="1419" d="M0 0l2 94q27 6 68 12q39 6 69.5 13.5t55.5 17.5q8 14 13 26.5t7 24.5q6 33 8 81t2 112l-2 498q-2 39 -3 139t-5 266q-4 88 -12 109q-4 8 -13 10q-20 14 -69 16q-23 0 -115 13l-4 84l262 6l383 12h45q8 2 15.5 2h13.5t21.5 -1t39.5 -1h76q92 0 193 -27q18 -4 42.5 -13 t53.5 -26q63 -31 104 -75q45 -47 66 -105q10 -29 15 -58.5t5 -62.5q0 -72 -32 -129q-31 -57 -95 -104q-16 -12 -54 -30.5t-97 -47.5q178 -41 268 -145q92 -104 92 -236q0 -72 -28 -162q-23 -66 -72 -116q-66 -72 -141 -109q-78 -35 -205 -59q-70 -12 -199 -11l-199 5 q-63 2 -138 -2.5t-163 -10.5q-25 -2 -93 -4t-181 -6zM537 1419q0 -12 1 -31.5t3 -44.5q2 -51 4 -119.5t0 -158.5v-98v-78q25 -4 52.5 -6t57.5 -2q176 0 267 65q90 66 90 225q0 113 -86 187q-84 76 -258 76q-53 0 -131 -15zM545 457l4 -271q0 -16 10 -43q74 -33 141 -32 q131 0 220 41q82 39 122 112q18 37 28.5 82t10.5 100q0 113 -43 181q-59 94 -141 125q-80 33 -250 32q-37 0 -61.5 -3t-40.5 -7v-143v-174z" /> +<glyph unicode="" horiz-adv-x="1054" d="M0 0l18 84q12 4 32 9t46 11q41 10 71 19.5t50 19.5q29 39 41 103l29 137l57 268l12 64q23 119 41.5 178t18.5 63l30 156l17 64l22 135l9 49v39q-45 23 -148 28q-14 0 -23.5 1.5t-17.5 1.5l21 104l325 -14q31 -2 49.5 -2h26.5q35 0 89 2t132 6q41 4 68.5 6t38.5 2 q-2 -10 -3 -19.5t-3 -19.5q-4 -10 -7.5 -22.5t-7.5 -28.5q-49 -16 -110 -31q-66 -16 -105 -31q-12 -33 -24 -88q-6 -25 -9.5 -45t-5.5 -37q-23 -100 -40 -175.5t-27 -129.5l-64 -311l-39 -158l-43 -235l-14 -45v-10.5t2 -16.5q35 -8 64.5 -13t58.5 -9q4 0 21.5 -2.5 l45.5 -6.5q-2 -18 -3 -32.5t-3 -26.5q-2 -6 -4 -16.5t-6 -22.5q-8 0 -14 -1t-10 -1q-18 -2 -28.5 -2h-14.5h-11.5t-17.5 4q-8 0 -45 4t-105 12l-202 2q-61 0 -181 -12q-39 -4 -63.5 -6t-36.5 -2z" /> +<glyph unicode="" d="M0 1151q14 37 34.5 110.5t45.5 184.5q8 33 13 54.5t9 31.5h58q4 -6 6 -10t4 -9q29 -57 41 -71q16 -4 129 -4q35 0 66.5 1t60.5 1l20 2l113 2l213 -2h289l55 10q10 8 27 53q2 6 4 12.5t6 16.5l43 2h10.5t16.5 -2q2 -39 1 -97.5t1 -138.5v-100v-57q0 -14 -1 -27.5t-3 -23.5 q-20 -8 -37 -11.5t-31 -7.5q-27 51 -53 129q-29 82 -37 92q-12 14 -27 21q-10 4 -60 4h-138h-31t-35 -4q-6 -43 -6 -72l2 -151v-334l2 -359v-147q0 -72 10 -117q8 -4 21.5 -8t34.5 -8q4 0 21 -4t50 -13q27 -10 49 -18q4 -20 4 -33.5v-17.5v-11.5t-2 -17.5h-34q-47 0 -88 2 t-76 6t-95.5 6t-148.5 2q-16 0 -57 -4t-109 -10q-29 -2 -45 -3t-24 -1q0 10 -1.5 16.5t-1.5 10.5l-2 24v10q18 31 80 50q94 27 135 49q4 10 6.5 25.5t4.5 31.5q4 68 6 176.5t0 255.5l-4 428q-2 90 -2 142.5t-4 72.5q0 8 -7 15q-4 6 -12 6q-16 4 -63 4h-127q-90 0 -119 -21 q-41 -29 -121 -153q-23 -35 -35 -35q-23 12 -36 23.5t-19 19.5zM1383 1305.5q-5 13.5 14 33.5l184 185q14 12 33 12q14 0 31 -12l184 -185q18 -20 13 -33.5t-34 -13.5h-118v-1048h118q29 0 34 -13.5t-13 -31.5l-184 -187q-16 -12 -33 -12q-16 0 -31 12l-184 187 q-18 18 -13 31.5t31 13.5h121v1048h-121q-27 0 -32 13.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 233q0 18 12 31l187 185q18 20 31.5 14t13.5 -33v-121h1048v121q0 27 13.5 33t33.5 -14l185 -185q12 -12 12 -31q0 -18 -12 -30l-185 -187q-20 -18 -33.5 -13t-13.5 34v119h-1048v-119q0 -29 -13.5 -34t-31.5 13l-187 187q-12 12 -12 30zM0 1233q14 29 33.5 87 t44.5 146q6 27 11 43.5t9 26.5h56q8 -12 10 -14q27 -47 37 -58q2 0 35.5 -1t77.5 -1h90.5h74.5h123l19 2h108h203h416l53 6q12 10 24 46l4.5 9l6.5 13h39h28v-188v-80v-45q0 -12 -1 -21.5t-3 -19.5q-33 -10 -63 -15q-25 37 -52 103q-27 59 -34 74q-12 10 -27 14q-6 2 -42 3 t-85 1h-103.5h-97.5h-28.5t-34.5 -2q-2 -18 -3 -32.5t-1 -24.5l4 -445l-2 -119q0 -61 12 -92q12 -6 53 -12q4 0 20.5 -4t45.5 -10q14 -4 26.5 -7.5t22.5 -7.5q2 -16 3 -25.5t1 -13.5t-1 -10t-1 -14h-33q-94 0 -157 6q-66 6 -236 6q-14 0 -53 -3t-105 -7q-27 -2 -43 -3 t-24 -1q0 16 -2 20v21v8q20 27 73 39q90 20 132 41q4 8 6 19t4 26q0 18 1 70.5t1 120t-1 142t-2 139t-2 107.5t-1 47q0 8 -6 13q-2 2 -13 6q-14 2 -59 2h-123q-20 0 -62 -1t-85 -2t-78 -4t-41 -7q-41 -25 -117 -123q-20 -29 -33 -29q-23 10 -35 19.5t-18 15.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t55.5 22h1689q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM0 500v114q0 33 22.5 55.5t55.5 22.5h1075q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1075 q-33 0 -55.5 22.5t-22.5 55.5zM0 922v114q0 33 22.5 55.5t55.5 22.5h1536q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1536q-33 0 -55.5 21.5t-22.5 54.5zM0 1343v117q0 31 22.5 53.5t55.5 22.5h921q31 0 53.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53 t-53.5 -22h-921q-33 0 -55.5 22.5t-22.5 52.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t55.5 22h1689q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM78 922v114q0 33 21.5 55.5t54.5 22.5h1536q33 0 55 -22.5t22 -55.5v-114q0 -33 -22.5 -54.5t-54.5 -21.5h-1536 q-33 0 -54.5 21.5t-21.5 54.5zM307 500v114q0 33 22.5 55.5t55.5 22.5h1075q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1075q-33 0 -55.5 22.5t-22.5 55.5zM385 1343v117q0 31 21.5 53.5t54.5 22.5h921q33 0 55.5 -22.5t22.5 -53.5v-117 q0 -31 -22.5 -53t-55.5 -22h-921q-33 0 -54.5 22.5t-21.5 52.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t55.5 22h1689q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM154 922v114q0 33 22.5 55.5t54.5 22.5h1536q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1536 q-33 0 -55 21.5t-22 54.5zM614 500v114q0 33 22.5 55.5t55.5 22.5h1075q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1075q-33 0 -55.5 22.5t-22.5 55.5zM768 1343v117q0 31 22.5 53.5t55.5 22.5h921q31 0 53.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53 t-53.5 -22h-921q-33 0 -55.5 22.5t-22.5 52.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t55.5 22h1689q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM0 500v114q0 33 22.5 55.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1689 q-33 0 -55.5 22.5t-22.5 55.5zM0 922v114q0 33 22.5 55.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1689q-33 0 -55.5 21.5t-22.5 54.5zM0 1343v117q0 31 22.5 53.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -53.5v-117 q0 -31 -22.5 -53t-53.5 -22h-1689q-33 0 -55.5 22.5t-22.5 52.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t55.5 22h153q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-153q-33 0 -55.5 22.5t-22.5 55.5zM0 500v114q0 33 22.5 55.5t55.5 22.5h153q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-153 q-33 0 -55.5 22.5t-22.5 55.5zM0 922v114q0 33 22.5 55.5t55.5 22.5h153q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-153q-33 0 -55.5 21.5t-22.5 54.5zM0 1343v117q0 31 22.5 53.5t55.5 22.5h153q31 0 53.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53 t-53.5 -22h-153q-33 0 -55.5 22.5t-22.5 52.5zM461 78v115q0 31 22.5 53t55.5 22h1228q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-1228q-33 0 -55.5 22.5t-22.5 55.5zM461 500v114q0 33 22.5 55.5t55.5 22.5h1228q31 0 53.5 -22.5t22.5 -55.5v-114 q0 -33 -22.5 -55.5t-53.5 -22.5h-1228q-33 0 -55.5 22.5t-22.5 55.5zM461 922v114q0 33 22.5 55.5t55.5 22.5h1228q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1228q-33 0 -55.5 21.5t-22.5 54.5zM461 1343v117q0 31 22.5 53.5t55.5 22.5h1228 q31 0 53.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53t-53.5 -22h-1228q-33 0 -55.5 22.5t-22.5 52.5z" /> +<glyph unicode="" d="M0 756v75q0 39 39 39h213v154q0 31 16.5 37t38.5 -17l215 -215q16 -16 17 -36q0 -18 -17 -35l-215 -215q-23 -23 -39 -17t-16 39v152h-213q-39 0 -39 39zM614 39v1458q0 39 39 39h76q39 0 39 -39v-1458q0 -39 -39 -39h-76q-39 0 -39 39zM922 78v115q0 31 22.5 53t54.5 22 h768q31 0 53.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-53.5 -22.5h-768q-33 0 -55 22.5t-22 55.5zM922 500v114q0 33 22.5 55.5t54.5 22.5h615q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-615q-33 0 -55 22.5t-22 55.5zM922 922v114 q0 33 22.5 55.5t54.5 22.5h691q33 0 55 -22.5t22 -55.5v-114q0 -33 -22.5 -54.5t-54.5 -21.5h-691q-33 0 -55 21.5t-22 54.5zM922 1343v117q0 31 22.5 53.5t54.5 22.5h537q33 0 55.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53t-55.5 -22h-537q-33 0 -55 22.5t-22 52.5z" /> +<glyph unicode="" d="M0 78v115q0 31 22.5 53t53.5 22h768q33 0 55.5 -22.5t22.5 -52.5v-115q0 -33 -22.5 -55.5t-55.5 -22.5h-768q-31 0 -53.5 22.5t-22.5 55.5zM0 500v114q0 33 22.5 55.5t53.5 22.5h614q33 0 55.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-55.5 -22.5h-614 q-31 0 -53.5 22.5t-22.5 55.5zM0 922v114q0 33 22.5 55.5t53.5 22.5h692q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-692q-31 0 -53.5 21.5t-22.5 54.5zM0 1343v117q0 31 22.5 53.5t53.5 22.5h538q31 0 53.5 -22.5t22.5 -53.5v-117q0 -31 -22.5 -53 t-53.5 -22h-538q-31 0 -53.5 22.5t-22.5 52.5zM1075 39v1458q0 39 39 39h76q16 0 27.5 -11.5t11.5 -27.5v-1458q0 -16 -11.5 -27.5t-27.5 -11.5h-76q-39 0 -39 39zM1305 743q0 16 14 35l217 215q23 23 38 17t15 -39v-152h215q16 0 27.5 -11t11.5 -28v-75q0 -16 -11 -27.5 t-28 -11.5h-215v-154q0 -31 -15 -38t-38 15l-217 218q-14 18 -14 36z" /> +<glyph unicode="" d="M0 324v768q0 47 18.5 89t50 72.5t73.5 49t89 18.5h768q47 0 89 -18.5t73 -49t49.5 -72.5t18.5 -89v-240l483 471q23 23 55 23q13 -1 29 -7q47 -20 47 -69v-1127q0 -49 -47 -69q-16 -6 -29 -6q-33 0 -55 22l-483 471v-237q0 -47 -18.5 -89t-49.5 -74t-72.5 -50.5 t-89.5 -18.5h-768q-47 0 -89 18.5t-73.5 50.5t-50 73.5t-18.5 89.5z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 154h1536v1228h-1536v-1228zM307 307v105l277 360l188 -156l354 537l410 -424v-422h-1229zM307 1073q0 66 45 111t111 45q63 0 108 -45 t45 -111q0 -63 -45 -108t-108 -45q-66 0 -111 45t-45 108z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 0l137 418l867 866l280 -280l-866 -867zM287 407.5q0 -12.5 10 -22.5q8 -8 22 -8q12 0 21 8l690 690q20 20 0 43q-10 10 -22.5 10t-20.5 -10l-690 -688q-10 -10 -10 -22.5zM1102 1382l119 119q35 35 84 35t84 -35l57 -55l55 -57q35 -35 35 -84.5t-35 -83.5l-119 -119z " /> +<glyph unicode="" horiz-adv-x="1128" d="M0 1001q0 117 44 220.5t121 180.5t180.5 121t219.5 44q117 0 219.5 -44t179 -121t120.5 -180.5t44 -220.5q0 -84 -24.5 -159.5t-65.5 -143.5l-379 -661q-41 -68 -95 -68t-93 68l-381 663q-41 68 -65.5 143t-24.5 158zM285 1001q0 -57 21.5 -108t60.5 -89t89 -59.5 t109 -21.5q57 0 108.5 21.5t89.5 59.5t59.5 89t21.5 108t-21.5 108.5t-59.5 90.5t-89 60.5t-109 21.5q-59 0 -109 -21.5t-89 -60.5t-60.5 -90t-21.5 -109z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44v1118q-117 0 -218.5 -44 t-177 -120t-119.5 -177t-44 -218z" /> +<glyph unicode="" horiz-adv-x="1130" d="M0 535q0 86 23.5 161.5t66.5 141.5q20 33 69.5 99.5t108 154.5t113.5 193.5t90 217.5q10 35 37 51.5t57 12.5q31 4 57.5 -12.5t36.5 -51.5q33 -113 89.5 -218t115 -193t107.5 -154.5t69 -99.5q43 -66 66.5 -141.5t23.5 -161.5q0 -117 -44 -220.5t-120.5 -180.5t-180 -121 t-220.5 -44t-220 44t-180 121t-121 180.5t-44 220.5zM248 410q0 -59 41 -100.5t100 -41.5t100 41t41 101q0 43 -22 75q-6 8 -18.5 24.5t-26.5 39t-28.5 48.5t-22.5 54q-4 20 -23 17q-18 4 -24 -17q-8 -29 -21.5 -54.5t-28 -48t-27 -38.5t-18.5 -25q-23 -33 -22 -75z" /> +<glyph unicode="" d="M0 307v922q0 63 24.5 119.5t65.5 97.5t97.5 65.5t119.5 24.5h1075q4 0 10.5 -1t10.5 -1l-191 -191h-905q-47 0 -80.5 -33.5t-33.5 -80.5v-922q0 -47 33.5 -80.5t80.5 -33.5h1075q47 0 81 33.5t34 80.5v445l193 192v-637q0 -63 -25 -118.5t-67 -97.5t-97 -66.5t-119 -24.5 h-1075q-63 0 -119.5 24.5t-97.5 66.5t-65.5 97.5t-24.5 118.5zM631 324l108 329l652 652l221 -222l-651 -651zM866 629q6 -8 17 -8q10 0 16 8l512 510q18 18 0 35q-18 16 -35 0l-510 -510q-18 -18 0 -35zM1501 1415l92 94q29 29 68 29t65 -29l46 -45l45 -45 q27 -29 27.5 -66.5t-27.5 -66.5l-95 -92z" /> +<glyph unicode="" d="M0 307v922q0 63 24.5 119.5t65.5 97.5t97.5 65.5t119.5 24.5h836q-6 -31 -6 -63v-52q-164 -20 -310 -78h-520q-47 0 -80.5 -33.5t-33.5 -80.5v-922q0 -47 33.5 -80.5t80.5 -33.5h1075q47 0 81 33.5t34 80.5v111q16 10 31.5 21.5t32.5 27.5l129 127v-287q0 -63 -25 -118.5 t-67 -97.5t-97 -66.5t-119 -24.5h-1075q-63 0 -119.5 24.5t-97.5 66.5t-65.5 97.5t-24.5 118.5zM385 388.5v37.5q0 166 56.5 312.5t173 256t293.5 173t419 65.5v231q0 57 28.5 69.5t69.5 -28.5l392 -391q27 -25 26 -65q0 -39 -26 -64l-392 -391q-41 -41 -69.5 -28.5 t-28.5 69.5v260q-207 0 -364.5 -43t-266 -116.5t-170 -174t-77.5 -215.5q-4 -27 -31 -27q-25 0 -29 27q-4 23 -4 42.5z" /> +<glyph unicode="" d="M0 307v922q0 63 24.5 119.5t65.5 97.5t97.5 65.5t119.5 24.5h1075q27 0 52 -6l-187 -187h-940q-47 0 -80.5 -33.5t-33.5 -80.5v-922q0 -47 33.5 -80.5t80.5 -33.5h1075q47 0 81 33.5t34 80.5v326l193 192v-518q0 -63 -25 -118.5t-67 -97.5t-97 -66.5t-119 -24.5h-1075 q-63 0 -119.5 24.5t-97.5 66.5t-65.5 97.5t-24.5 118.5zM385 966.5q0 32.5 23 55.5l98 98q23 23 55.5 23t54.5 -23l340 -340l654 656q23 23 56.5 22.5t55.5 -22.5l99 -99q23 -23 22.5 -55.5t-22.5 -54.5l-711 -711l-98 -98q-23 -23 -55.5 -23t-55.5 23l-100 98l-393 395 q-23 23 -23 55.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 25 18 43l256 256q27 27 45.5 19.5t18.5 -46.5v-170h360v359h-172q-37 0 -45 18.5t19 44.5l256 256q18 18 43 19q25 0 43 -19l256 -256q27 -27 19.5 -45t-46.5 -18h-172v-359h361v170q0 39 18 46.5t45 -19.5l256 -256q18 -18 18 -43t-18 -43l-256 -256 q-27 -27 -45 -19.5t-18 46.5v176h-361v-365h172q39 0 47 -18.5t-20 -44.5l-256 -256q-18 -18 -43 -19q-25 0 -43 19l-256 256q-27 27 -20 45t46 18h172v365h-360v-176q0 -39 -18.5 -46.5t-45.5 19.5l-256 256q-18 18 -18 43z" /> +<glyph unicode="" horiz-adv-x="1075" d="M0 76q0 -31 22.5 -53.5t53.5 -22.5h153q33 0 55.5 22.5t22.5 53.5v1382q0 33 -22.5 55.5t-55.5 22.5h-153q-31 0 -53.5 -22.5t-22.5 -55.5v-1382zM307.5 768q-0.5 27 16.5 43l653 707q14 18 41 18q6 0 22 -4q35 -18 35 -59v-1412q0 -41 -35 -57q-39 -14 -63 14l-653 705 q-16 18 -16.5 45z" /> +<glyph unicode="" d="M0 76q0 -31 22.5 -53.5t53.5 -22.5h153q33 0 55.5 22.5t22.5 53.5v1382q0 33 -22.5 55.5t-55.5 22.5h-153q-31 0 -53.5 -22.5t-22.5 -55.5v-1382zM307.5 768q-0.5 27 16.5 43l653 707q14 18 41 18q6 0 22 -4q35 -18 35 -59v-1412q0 -41 -35 -57q-39 -14 -63 14l-653 705 q-16 18 -16.5 45zM1075.5 768q-0.5 27 16.5 43l653 707q14 18 41 18q6 0 22 -4q35 -18 35 -59v-1412q0 -41 -35 -57q-39 -14 -63 14l-653 705q-16 18 -16.5 45z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 767q0 26 16 44l654 707q14 18 41 18q10 0 22 -6q35 -14 35 -57v-1412q0 -41 -35 -57q-37 -16 -63 14l-654 705q-16 18 -16 44zM768 767q0 26 16 44l654 707q14 18 41 18q10 0 22 -6q35 -14 35 -57v-1412q0 -41 -35 -57q-37 -16 -63 14l-654 705q-16 18 -16 44z" /> +<glyph unicode="" horiz-adv-x="1349" d="M0 70v1396q0 39 35 60q37 23 69 0l1211 -697q35 -25 35 -61q0 -37 -35 -61l-1211 -697q-16 -10 -34 -10t-35 10q-35 20 -35 60z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 70v1396q0 29 20.5 49.5t49.5 20.5h489q29 0 49.5 -20.5t20.5 -49.5v-1396q0 -29 -20.5 -49.5t-49.5 -20.5h-489q-29 0 -49.5 20.5t-20.5 49.5zM907 70v1396q0 29 20.5 49.5t49.5 20.5h489q29 0 49.5 -20.5t20.5 -49.5v-1396q0 -29 -20.5 -49.5t-49.5 -20.5h-489 q-29 0 -49.5 20.5t-20.5 49.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 70v1396q0 29 20.5 49.5t49.5 20.5h1396q29 0 49.5 -20.5t20.5 -49.5v-1396q0 -29 -20.5 -49.5t-49.5 -20.5h-1396q-29 0 -49.5 20.5t-20.5 49.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 61v1414q0 39 35 57q39 14 63 -14l654 -705q16 -18 16 -45t-16 -45l-654 -705q-16 -18 -41 -18q-6 0 -22 4q-35 16 -35 57zM768 61v1414q0 39 35 57q39 14 63 -14l654 -705q16 -18 16 -45t-16 -45l-654 -705q-16 -18 -41 -18q-6 0 -22 4q-35 16 -35 57z" /> +<glyph unicode="" d="M0 61v1414q0 39 35 57q39 14 63 -14l654 -705q16 -18 16 -45t-16 -45l-654 -705q-16 -18 -41 -18q-6 0 -22 4q-35 16 -35 57zM768 61v1414q0 39 35 57q39 14 63 -14l654 -705q16 -18 16 -45t-16 -45l-654 -705q-16 -18 -41 -18q-6 0 -22 4q-35 16 -35 57zM1536 76 q0 -31 22.5 -53.5t53.5 -22.5h153q33 0 55.5 22.5t22.5 53.5v1382q0 33 -22.5 55.5t-55.5 22.5h-153q-31 0 -53.5 -22.5t-22.5 -55.5v-1382z" /> +<glyph unicode="" horiz-adv-x="1075" d="M0 61v1414q0 39 35 57q39 14 63 -14l654 -705q16 -18 16 -45t-16 -45l-654 -705q-16 -18 -41 -18q-6 0 -22 4q-35 16 -35 57zM768 76q0 -31 22.5 -53.5t53.5 -22.5h153q33 0 55.5 22.5t22.5 53.5v1382q0 33 -22.5 55.5t-55.5 22.5h-153q-31 0 -53.5 -22.5t-22.5 -55.5 v-1382z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 70v198q0 29 20.5 49.5t49.5 20.5h1396q29 0 49.5 -20.5t20.5 -49.5v-198q0 -29 -20.5 -49.5t-49.5 -20.5h-1396q-29 0 -49.5 20.5t-20.5 49.5zM6 594q-18 43 14 76l699 698q20 20 49 20.5t49 -20.5l699 -698q33 -33 14 -76q-16 -43 -64 -43h-1396q-47 0 -64 43z" /> +<glyph unicode="" horiz-adv-x="964" d="M0 765q0 38 29 66l671 674q29 29 68 29t68 -29l100 -100q29 -29 29 -68t-29 -67l-508 -510l508 -500q29 -29 29 -66.5t-29 -66.5l-100 -102q-29 -29 -68 -29t-68 29l-671 673q-29 29 -29 67z" /> +<glyph unicode="" horiz-adv-x="964" d="M0 194.5q0 38.5 29 67.5l508 508l-508 502q-29 29 -29 66.5t29 66.5l100 102q29 29 68 29t67 -29l672 -673q29 -29 29 -68t-29 -68l-672 -671q-29 -29 -67.5 -29t-67.5 29l-100 100q-29 29 -29 67.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM338 688q0 -33 33 -33h313v-344q0 -33 33 -32h164q33 0 32 32v344h314 q12 0 22.5 9.5t10.5 23.5v158q0 14 -10.5 23.5t-22.5 9.5h-314v346q0 33 -32 32h-164q-33 0 -33 -32v-346h-313q-33 0 -33 -33v-158z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM338 688q0 -33 33 -33h856q12 0 22.5 9.5t10.5 23.5v158q0 14 -10.5 23.5 t-22.5 9.5h-856q-33 0 -33 -33v-158z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 770q0 154 58.5 301.5t174.5 263.5q117 117 264.5 175.5t301 58.5t301 -58.5t264.5 -175.5t175 -264t58 -301t-58 -301t-175 -264t-264.5 -175.5t-301 -58.5t-301 58.5t-264.5 175.5t-175 264t-58 301zM385 505q0 -13 10 -24l115 -116q10 -10 23.5 -10.5t23.5 10.5 l244 243l219 -221q10 -10 23.5 -10t23.5 10l113 113q23 23 0 47l-222 219l246 246q23 23 0 47l-117 115q-25 25 -47 0l-243 -244l-222 221q-10 10 -23 10t-24 -10l-110 -113q-25 -23 0 -47l219 -219l-242 -244q-10 -10 -10 -23z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 718q0 -20 14 -34l319 -319q14 -14 38 -24.5t44 -10.5h56q20 0 43.5 10 t38.5 25l550 550q14 14 14.5 34t-14.5 34l-104 107q-16 14 -35.5 14t-34.5 -14l-452 -453q-14 -14 -33.5 -14t-34.5 14l-221 221q-14 14 -33.5 14t-36.5 -14l-104 -106q-14 -14 -14 -34z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM532 1100l95 -115q4 -8 20 -12q12 0 23 6l10 8t27.5 17.5t39 16.5t46.5 7 q41 0 69.5 -22.5t28.5 -57.5q0 -37 -24.5 -64.5t-61.5 -60.5q-23 -18 -46.5 -40.5t-43 -51.5t-31.5 -63.5t-12 -79.5v-64q0 -12 9 -21t21 -9h164q12 0 20.5 9t8.5 21v51q0 39 25.5 66.5t62.5 60.5q25 20 49.5 46t46 57t34.5 69.5t13 90.5q0 68 -27.5 121t-73.5 87.5 t-103.5 53t-114.5 18.5q-63 0 -113.5 -16.5t-85.5 -35.5t-53 -35.5t-20 -18.5q-16 -16 -3 -39zM672 252q0 -12 9 -21.5t21 -9.5h164q12 0 20.5 9.5t8.5 21.5v156q0 12 -8 21t-21 9h-164q-12 0 -21 -9t-9 -21v-156z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM573 858q0 -29 29 -29h86v-409h-78q-12 0 -21 -8.5t-9 -20.5v-139q0 -12 9 -21.5 t21 -9.5h402q12 0 20 9.5t8 21.5v139q0 29 -28 29h-78v577q0 12 -8 21.5t-21 9.5h-303q-12 0 -20.5 -9t-8.5 -22v-139zM686 1151q0 -12 9.5 -21.5t21.5 -9.5h188q12 0 20.5 9.5t8.5 21.5v166q0 29 -29 29h-188q-12 0 -21.5 -8.5t-9.5 -20.5v-166z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 692v154q0 16 11.5 26.5t27.5 10.5h164q18 84 59 158.5t99.5 133t133 99.5t158.5 59v164q0 39 39 39h154q16 0 26.5 -11.5t10.5 -27.5v-164q84 -18 158.5 -59t133 -99.5t99.5 -133t59 -158.5h164q16 0 27.5 -10.5t11.5 -26.5v-154q0 -39 -39 -39h-164 q-18 -84 -59 -158.5t-99.5 -133t-133 -99.5t-158.5 -59v-164q0 -16 -10.5 -27.5t-26.5 -11.5h-154q-39 0 -39 39v164q-84 18 -158.5 59t-133 99.5t-99.5 133t-59 158.5h-164q-39 0 -39 39zM365 653q29 -106 105.5 -183t182.5 -105v174q0 16 11.5 26t27.5 10h154 q16 0 26.5 -10t10.5 -26v-174q106 29 183 105.5t105 182.5h-172q-39 0 -38 39v154q0 16 11 26.5t27 10.5h172q-29 106 -105.5 183t-182.5 105v-172q0 -16 -10.5 -27t-26.5 -11h-154q-39 0 -39 38v172q-106 -29 -183 -105.5t-105 -182.5h174q16 0 26 -10.5t10 -26.5v-154 q0 -16 -10 -27.5t-26 -11.5h-174z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177.5 120t-217.5 44q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM451 573.5q0 16.5 12 28.5l166 166l-166 166q-12 12 -12 28.5t12 28.5l112 113q29 29 58 0l166 -166l166 166q29 29 57 0l113 -113q29 -29 0 -57l-166 -166l166 -166 q29 -29 0 -57l-113 -113q-12 -12 -28.5 -12t-28.5 12l-166 166l-166 -166q-12 -12 -28.5 -12t-29.5 12l-112 113q-12 12 -12 28.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -117 44 -218t119.5 -177t177 -120t218.5 -44t218 44t177 120t120 177 t44 218t-44 218t-120 177t-177.5 120t-217.5 44q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM332 717.5q0 17.5 10 27.5l115 115q10 10 27.5 10t27.5 -10l178 -180q29 -25 58 0l337 340q10 10 27.5 10t28.5 -10l114 -115q10 -10 10.5 -27.5t-10.5 -27.5l-409 -410 q-12 -12 -32.5 -20t-37.5 -8h-114q-16 0 -37 8t-33 20l-250 250q-10 10 -10 27.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -84 23.5 -160t66.5 -141l770 770q-66 43 -141.5 66.5t-159.5 23.5 q-117 0 -218.5 -44t-177 -120t-119.5 -177t-44 -218zM498 297q66 -41 141.5 -64.5t159.5 -23.5q117 0 218 44t177 120t120 177t44 218q0 84 -23.5 160t-64.5 141z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 768q0 39 29 68l671 673q29 29 68 29t68 -29l100 -100q29 -29 29 -67.5t-29 -67.5l-338 -338h842q41 0 68.5 -27.5t27.5 -68.5v-144q0 -39 -27.5 -66.5t-66.5 -27.5h-844l338 -338q29 -29 29 -67.5t-29 -67.5l-100 -100q-29 -29 -68 -29t-68 29l-671 671q-29 29 -29 68 z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 698v144q0 39 27.5 66.5t66.5 27.5h844l-338 338q-29 29 -29 67.5t29 67.5l100 100q29 29 68 29t68 -29l671 -673q29 -29 29 -67t-29 -67l-671 -673q-29 -29 -68 -29t-68 29l-100 100q-29 29 -29 68t29 67l338 338h-844q-39 0 -66.5 27.5t-27.5 68.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M-1 768q-1 39 28 68l673 671q29 29 68 29t68 -29l671 -671q29 -29 29 -68t-29 -68l-100 -100q-29 -29 -66.5 -29t-66.5 29l-340 338v-844q0 -39 -27.5 -66.5t-66.5 -27.5h-144q-41 0 -67.5 27.5t-26.5 66.5v844l-338 -338q-29 -29 -67.5 -29t-67.5 29l-100 100 q-29 29 -30 68z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 766q0 39 29 68l100 100q29 29 68 29t67 -29l338 -338v844q0 39 27.5 66.5t66.5 27.5h144q41 0 67.5 -27.5t26.5 -66.5v-844l340 338q29 29 66.5 29t66.5 -29l102 -100q29 -29 29 -68t-29 -68l-673 -671q-29 -29 -68 -29t-68 29l-671 671q-29 29 -29 68z" /> +<glyph unicode="" d="M0 135q0 209 71.5 393.5t218 322.5t371 219t531.5 83v293q0 72 36 87t89 -36l491 -493q35 -33 35 -82q0 -47 -35 -82l-491 -494q-51 -51 -88 -35.5t-37 86.5v330q-262 -2 -461 -56.5t-336 -147.5t-215 -219t-98 -271q-4 -33 -37 -33h-2q-33 0 -37 33q-6 51 -6 102z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 86v522q0 53 26.5 64.5t65.5 -25.5l166 -166l274 275q12 12 31 12t33 -12l160 -160q12 -14 12 -32.5t-12 -31.5l-275 -274l166 -166q39 -39 27 -65.5t-66 -26.5h-520q-37 0 -61 25q-27 27 -27 61zM768 972.5q0 18.5 12 31.5l275 274l-166 166q-39 39 -27 65.5t66 26.5 h520q37 0 61 -25q27 -27 27 -61v-522q0 -53 -26.5 -64.5t-65.5 25.5l-166 166l-274 -273q-12 -14 -31 -14t-33 14l-160 158q-12 14 -12 32.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 202.5q0 18.5 12 33.5l275 272l-166 166q-39 39 -27 65.5t66 26.5h520q38 0 61 -25q27 -27 27 -61v-522q0 -53 -26.5 -64.5t-65.5 27.5l-166 166l-274 -275q-12 -14 -31 -14t-33 14l-160 160q-12 12 -12 30.5zM768 854v522q0 53 26.5 64.5t65.5 -27.5l166 -166l274 275 q12 14 31 14t33 -14l160 -160q12 -12 12 -30.5t-12 -33.5l-275 -272l166 -166q39 -39 27 -65.5t-66 -26.5h-520q-41 0 -61 25q-27 27 -27 61z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 696v144q0 39 27.5 66.5t66.5 27.5h508v506q0 41 26.5 68.5t67.5 27.5h144q39 0 66.5 -27.5t27.5 -66.5v-508h508q39 0 66.5 -27.5t27.5 -66.5v-144q0 -39 -27.5 -66.5t-66.5 -27.5h-508v-506q0 -41 -27.5 -68.5t-66.5 -27.5h-144q-39 0 -66.5 27.5t-27.5 66.5v508 h-506q-41 0 -68.5 26.5t-27.5 67.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 696v144q0 39 27.5 66.5t66.5 27.5h1348q39 0 66.5 -27.5t27.5 -66.5v-144q0 -39 -27.5 -66.5t-66.5 -27.5h-1346q-41 0 -68.5 26.5t-27.5 67.5z" /> +<glyph unicode="" horiz-adv-x="1427" d="M2 1018q-10 37 10 72l72 124q20 35 58 44.5t73 -9.5l332 -192v383q0 41 27.5 68.5t68.5 27.5h141q41 0 69 -27.5t28 -66.5v-385l331 192q35 18 73 9t58 -44l70 -124q20 -35 11 -72t-44 -57l-333 -193l333 -193q35 -20 44.5 -57t-9.5 -72l-72 -124q-20 -35 -58 -44.5 t-73 9.5l-331 192v-383q0 -41 -28 -68.5t-69 -27.5h-141q-41 0 -68.5 27.5t-27.5 66.5v385l-332 -192q-35 -20 -73 -10t-58 45l-72 124q-18 35 -9 72t44 57l334 193l-334 193q-35 20 -45 57z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM672 1274l14 -739q4 -29 31 -29h162q12 0 21 8t9 21l17 739q0 10 -8 22q-8 8 -23 9 h-193q-14 0 -22 -9q-8 -12 -8 -22zM680 236q0 -12 8 -21.5t21 -9.5h180q12 0 21.5 9t9.5 22v174q0 12 -9.5 21t-21.5 9h-180q-12 0 -20.5 -9t-8.5 -21v-174z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 588v391q0 16 11.5 27.5t27.5 11.5h539q-57 0 -107.5 21.5t-87.5 58.5t-58.5 87t-21.5 107t21.5 107.5t58.5 87.5t87 58.5t108 21.5q61 0 115.5 -24.5t88.5 -69.5l158 -203l158 203q35 45 89 69.5t116 24.5q57 0 107 -21.5t87 -58.5t58.5 -87t21.5 -108 q0 -57 -21.5 -107t-58.5 -87t-87 -58.5t-107 -21.5h538q16 0 27.5 -11.5t11.5 -27.5v-391q0 -16 -11 -27.5t-28 -11.5h-117v-432q0 -47 -34.5 -82t-83.5 -35h-1332q-49 0 -82.5 35t-33.5 82v432h-119q-16 0 -27.5 11t-11.5 28zM461 1292q0 -49 33.5 -82.5t83.5 -33.5h237 l-151 196q-10 10 -32 24.5t-54 14.5q-49 0 -83 -35t-34 -84zM743 221q0 -33 24 -56.5t56 -23.5h234q33 0 56.5 23.5t23.5 56.5v797h-394v-797zM1065 1176h238q49 0 82.5 33.5t33.5 82.5t-33.5 84t-82.5 35q-33 0 -54.5 -14.5t-31.5 -24.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M6 135q20 53 56 89t71 65q29 23 50.5 43t27.5 43q2 6 0 12t-10 25q-6 12 -11.5 27.5t-9.5 35.5q-25 160 13 295t120 242.5t195 185.5t235 121q82 29 179.5 34t203.5 7q61 0 127 2t127.5 11t113.5 28.5t85 54.5q20 20 38.5 41t38 36t43 24.5t58.5 9.5q23 0 42.5 -11.5 t29.5 -31.5q66 -133 83 -281.5t-18 -322.5q-90 -487 -579 -740q-231 -123 -467 -123q-154 0 -307 54q-23 8 -45.5 20t-44.5 25q-29 16 -57.5 30.5t-49.5 14.5q-10 -2 -23.5 -17.5t-27.5 -35t-26.5 -41t-20.5 -33.5q-14 -23 -26.5 -41.5t-22.5 -32.5q-25 -31 -64 -31h-4 q-29 2 -49.5 12.5t-33.5 24.5t-20.5 28.5t-9.5 22.5q-27 37 -10 78zM401 507.5q2 -32.5 29 -54.5q20 -18 51 -19q37 0 62 27q90 102 181 175t191.5 117t212 62t242.5 14q33 -4 57.5 19.5t26.5 56.5q2 35 -20.5 58.5t-57.5 25.5q-147 6 -277 -16.5t-247 -73.5t-223.5 -133 t-206.5 -199q-23 -27 -21 -59.5z" /> +<glyph unicode="" horiz-adv-x="1445" d="M0 442q0 123 63.5 254t174.5 238q10 16 33.5 13t31.5 -21q4 -16 0 -29q-8 -35 -14 -84t-4 -101.5t13 -101.5t38 -84q31 -37 78 -49q-49 154 -40 293t49 259t101.5 217t118.5 167t98 106.5t43 39.5q25 18 47 0q10 -8 13.5 -21.5t-0.5 -24.5q0 -2 -15.5 -42.5t-21.5 -102 t7 -134.5t71 -136q37 -43 67.5 -85t52 -93.5t34 -120t12.5 -162.5q0 -33 -31 -39q-12 -2 -24.5 4t-16.5 19q-14 31 -42 49t-63 18q-47 0 -79.5 -33.5t-32.5 -81.5q0 -121 156 -120q98 0 153 63q37 43 53.5 101.5t20.5 114.5t2 102.5t-4 62.5q-6 12 0 29q8 18 31.5 21 t35.5 -13q111 -106 173.5 -237.5t62.5 -254.5q0 -109 -51.5 -195.5t-145.5 -148t-228 -95.5t-298 -34t-298 34t-228.5 95.5t-145.5 148.5t-51 195z" /> +<glyph unicode="" d="M0 722q0 44 25 83q78 125 178 224t216 168t242.5 104.5t260.5 35.5q135 0 262 -35.5t241.5 -103.5t215 -167t180.5 -226q23 -39 22.5 -83t-22.5 -81q-80 -127 -180.5 -226.5t-215 -167t-241.5 -103t-262 -35.5q-133 0 -260 35.5t-243 104t-216.5 168t-177.5 224.5 q-25 37 -25 81zM154 723q66 -104 149.5 -190.5t182 -145.5t208 -92t228.5 -33t228.5 33t207.5 92t182 145t150 191q-80 129 -187.5 226.5t-236.5 156.5q51 -59 79.5 -133t28.5 -160q0 -96 -35.5 -179t-99 -146.5t-147.5 -99.5t-179 -36q-96 0 -179 36t-146.5 99.5t-99 146.5 t-35.5 179q0 78 25.5 148.5t68.5 128.5q-117 -59 -217.5 -152.5t-175.5 -214.5zM614 813q0 -23 16.5 -40t41.5 -17t41 17.5t16 39.5q0 78 53.5 131t130.5 53q25 0 41.5 17.5t16.5 40.5q0 25 -16.5 41t-41.5 16q-61 0 -116.5 -23.5t-95 -63.5t-63.5 -95.5t-24 -116.5z" /> +<glyph unicode="" d="M0 767q0 44 25 83q78 125 178 224.5t216 168t242.5 104t260.5 35.5q53 0 105 -7t103 -17l89 158q8 14 24 18q12 6 29 -2l133 -76q14 -8 19.5 -23.5t-3.5 -29.5l-774 -1383q-6 -14 -22 -18q-4 -2 -11 -2q-6 0 -18 4l-135 76q-14 8 -18.5 23.5t3.5 29.5l66 115 q-145 68 -269 178.5t-218 259.5q-25 37 -25 81zM154 768q82 -131 191.5 -229.5t240.5 -157.5l57 104q-86 63 -138 159.5t-52 213.5q0 78 25.5 148.5t68.5 130.5q-117 -61 -217.5 -153.5t-175.5 -215.5zM614 858q0 -23 16.5 -40t41.5 -17t41 17.5t16 39.5q0 78 53.5 131 t130.5 53q25 0 41.5 17.5t16.5 40.5q0 25 -16.5 41t-41.5 16q-61 0 -116.5 -23.5t-95 -63.5t-63.5 -95.5t-24 -116.5zM928 154l88 159q209 25 382 145t292 310q-109 170 -263 283l76 137q92 -66 172 -150t146 -188q23 -39 22.5 -83t-22.5 -81q-160 -254 -392.5 -392 t-500.5 -140zM1081 430l285 510q4 -20 6 -39.5t2 -42.5q0 -74 -21.5 -140.5t-60.5 -121.5t-93 -98t-118 -68z" /> +<glyph unicode="" horiz-adv-x="1775" d="M0 92q0 20 6 39t17 35l768 1331q16 27 39.5 48.5t56.5 21.5t56.5 -21.5t39.5 -48.5l770 -1331q10 -16 16.5 -34.5t6.5 -39.5q0 -49 -38 -70.5t-81 -21.5h-1538q-43 0 -81 21.5t-38 70.5zM770 1114l12 -633q0 -14 10.5 -24.5t24.5 -10.5h139q14 0 24.5 10.5t10.5 24.5 l15 633q0 14 -10.5 24.5t-24.5 10.5h-166q-16 0 -25.5 -10.5t-9.5 -24.5zM776 168q0 -16 10.5 -25.5t24.5 -9.5h154q35 0 34 35v147q0 16 -9 26.5t-25 10.5h-154q-14 0 -24.5 -10t-10.5 -27v-147z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 541v4q0 31 23 57l30 29q20 25 56 24q8 0 10 -2l280 -32q39 63 93.5 134.5t125.5 151.5l-583 457q-31 25 -31 59v4q0 31 23 58l57 57q27 23 57 23h13.5t13.5 -4l803 -293l151 149q70 70 161 110t167 40q72 0 109 -37q20 -18 28 -46t8 -63q0 -76 -38.5 -167t-110.5 -160 l-150 -152l293 -803q6 -12 6 -26q0 -33 -24 -58l-58 -57q-23 -25 -57 -25h-4q-35 4 -59 31l-455 586q-82 -72 -153.5 -126t-135.5 -93l35 -281v-10q0 -33 -22 -55l-31 -31q-23 -23 -58 -23h-4q-39 4 -59 31l-205 272l-274 207q-29 25 -31 60z" /> +<glyph unicode="" horiz-adv-x="1566" d="M0 117v1147q0 47 35 81.5t82 34.5h39v13q0 31 10 62.5t32.5 56t59.5 40t90 15.5t90 -15.5t59.5 -40t33 -56.5t10.5 -62v-13h49v13q0 31 10 62.5t32.5 56t59.5 40t90 15.5t90 -15.5t60.5 -40t34 -56.5t10.5 -62v-13h49v13q0 31 10.5 62.5t33 56t59 40t90.5 15.5 q53 0 90 -15.5t59.5 -40t32.5 -56.5t10 -62v-13h39q47 0 82 -34.5t35 -81.5v-1147q0 -47 -35 -82t-82 -35h-1333q-47 0 -82 35t-35 82zM158 158h282v250h-282v-250zM158 446h282v252h-282v-252zM158 737h282v252h-282v-252zM272 1165q0 -57 76 -57t76 57v228q0 57 -76 57 t-76 -57v-228zM479 158h285v250h-285v-250zM479 446h285v252h-285v-252zM479 737h285v252h-285v-252zM709 1165q0 -31 16 -44t57 -13t58.5 13.5t17.5 43.5v228q0 29 -17.5 43t-58.5 14t-57 -14.5t-16 -42.5v-228zM803 158h284v250h-284v-250zM803 446h284v252h-284v-252z M803 737h284v252h-284v-252zM1126 158h283v250h-283v-250zM1126 446h283v252h-283v-252zM1126 737h283v252h-283v-252zM1143 1165q0 -57 76 -57t75 57v228q0 57 -75 57q-76 0 -76 -57v-228z" /> +<glyph unicode="" d="M0 252v154q0 16 11.5 27t27.5 11h219q51 0 99.5 31t93.5 83t90 119.5t90 139.5q55 88 114.5 175t127 156t147.5 110.5t180 41.5h203v185q0 41 24.5 49t59.5 -21l334 -278q23 -18 22 -45q0 -29 -22 -47l-334 -277q-35 -29 -59.5 -20.5t-24.5 49.5v174h-203 q-53 0 -100 -31.5t-93 -84t-91.5 -120t-90.5 -139.5q-55 -88 -113.5 -174t-126 -154.5t-148.5 -110.5t-179 -42h-219q-16 0 -27.5 11.5t-11.5 27.5zM0 1108v154q0 39 39 38h219q68 0 127 -20t111.5 -56t97.5 -84t88 -104q-61 -90 -117 -178q-4 -8 -9 -14t-9 -15 q-70 102 -139.5 172t-149.5 70h-219q-16 0 -27.5 10.5t-11.5 26.5zM778 481q29 41 56.5 85t56.5 87q4 10 10 17.5t10 17.5q70 -102 139.5 -170.5t149.5 -68.5h203v190q0 41 24.5 49t59.5 -20l334 -277q23 -18 22 -47q0 -27 -22 -45l-334 -279q-35 -29 -59.5 -20.5 t-24.5 49.5v168h-203q-68 0 -127 20.5t-110.5 56.5t-97.5 84t-87 103z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 866q0 145 76 272.5t206 222.5t303.5 150.5t372.5 55.5t373 -55.5t304 -150.5t206 -222t76 -273q0 -145 -76 -272t-206 -222.5t-304 -150.5t-373 -55q-92 0 -180 12q-176 -139 -411 -192q-25 -4 -50.5 -8.5t-54.5 -8.5q-16 -2 -27.5 6.5t-15.5 24.5t5 26.5t20 20.5 q23 23 43 44.5t36.5 51t30 69.5t23.5 97q-174 98 -275.5 241.5t-101.5 315.5z" /> +<glyph unicode="" horiz-adv-x="1566" d="M0 621v288q0 16 11.5 27.5t27.5 11.5h391q16 0 28.5 -11t12.5 -28v-288q0 -29 22.5 -60t63.5 -58.5t98.5 -45t128.5 -17.5q70 0 128.5 17.5t99.5 45t63.5 58.5t22.5 60v288q0 39 39 39h391q16 0 27.5 -11t11.5 -28v-288q0 -135 -61.5 -254t-168 -207t-249 -139.5 t-304.5 -51.5q-164 0 -306 51.5t-248.5 139.5t-168 206.5t-61.5 254.5zM0 1106v391q0 16 11.5 27.5t27.5 11.5h391q16 0 28.5 -11.5t12.5 -27.5v-391q0 -16 -12.5 -27.5t-28.5 -11.5h-391q-16 0 -27.5 11.5t-11.5 27.5zM1098 1106v391q0 16 11 27.5t28 11.5h391 q16 0 27.5 -11.5t11.5 -27.5v-391q0 -16 -11.5 -27.5t-27.5 -11.5h-391q-39 0 -39 39z" /> +<glyph unicode="" horiz-adv-x="1536" d="M-1 355.5q-1 37.5 28 66.5l673 674q29 29 68 28.5t68 -28.5l671 -674q29 -29 29 -67t-29 -66l-100 -103q-29 -29 -68 -28.5t-67 28.5l-508 510l-500 -510q-29 -29 -67.5 -28.5t-67.5 28.5l-100 103q-29 29 -30 66.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 918.5q0 37.5 29 66.5l100 102q29 29 68 29t67 -29l508 -509l500 509q29 29 67.5 29t67.5 -29l100 -102q29 -29 30 -66.5t-28 -66.5l-673 -674q-29 -29 -68 -28.5t-68 28.5l-671 674q-29 29 -29 66.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M2.5 1038.5q-12.5 30.5 28.5 75.5l385 424q25 29 63 29q39 0 64 -29l385 -424q41 -45 28.5 -75.5t-67.5 -30.5h-250v-680h174q6 -10 12 -19.5t17 -19.5l252 -281h-695q-33 0 -56.5 23.5t-23.5 56.5v920h-249q-55 0 -67.5 30.5zM823 1526h695q33 0 56 -22.5t23 -57.5v-918 h250q55 0 67.5 -30.5t-28.5 -75.5l-385 -424q-25 -29 -63 -29q-39 0 -64 29l-385 424q-41 45 -28.5 75.5t67.5 30.5h250v678h-174q-6 10 -12.5 20.5t-14.5 20.5z" /> +<glyph unicode="" d="M0 1421v76q0 39 39 39h231q16 0 38 -4t36 -8q6 -4 14.5 -14.5t15.5 -23.5t12 -26.5t7 -21.5l27 -125h1347q35 0 58 -27t16 -59l-108 -578q-6 -25 -26.5 -42t-49.5 -17h-1084l35 -168q4 -16 17.5 -26.5t29.5 -10.5h856q16 0 27.5 -11.5t11.5 -27.5v-78q0 -16 -11 -26.5 t-28 -10.5h-163h-652h-104q-16 0 -36.5 3.5t-35.5 9.5q-6 2 -14 13t-15.5 24.5t-12.5 27t-7 21.5l-215 1016q-4 16 -17.5 26t-29.5 10h-170q-39 0 -39 39zM582 115q0 47 33.5 81.5t80.5 34.5q49 0 83 -34.5t34 -81.5t-34 -81t-83 -34q-47 0 -80.5 34t-33.5 81zM1233 115 q0 47 33.5 81.5t81.5 34.5q47 0 80.5 -34.5t33.5 -81.5t-33.5 -81t-80.5 -34t-81 34t-34 81z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h692q47 0 81 -34t34 -81t33.5 -80.5t80.5 -33.5h693q47 0 80.5 -35t33.5 -82v-1075q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81z" /> +<glyph unicode="" d="M0 379v1042q0 47 34 81t81 34h692q47 0 81 -34t34 -81t33.5 -80.5t80.5 -33.5h443q47 0 80.5 -35t33.5 -82v-221h-1228q-37 0 -72 -12.5t-65.5 -34t-53 -51t-35.5 -64.5zM43 0l246 760q6 23 30.5 39t47.5 16h1476l-260 -758q-6 -23 -30.5 -40t-47.5 -17h-1462z" /> +<glyph unicode="" horiz-adv-x="798" d="M2 368.5q10 22.5 57 22.5h203v754h-203q-47 0 -57 22.5t25 57.5l319 319q23 23 53 23q31 0 54 -23l321 -319q33 -35 22.5 -57.5t-57.5 -22.5h-202v-754h202q47 0 57.5 -22.5t-24.5 -57.5l-319 -319q-23 -23 -54 -23t-53 23l-321 319q-33 35 -23 57.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 729q0 31 23 53l319 322q35 33 57.5 23.5t22.5 -56.5v-205h754v203q0 47 22.5 57.5t56.5 -22.5l320 -322q23 -23 22 -53q0 -31 -22 -53l-320 -322q-35 -33 -57 -22.5t-22 57.5v203h-754v-203q0 -47 -22.5 -57t-57.5 24l-319 320q-23 23 -23 53z" /> +<glyph unicode="" d="M0 115v1306q0 47 35 81t82 34h1612q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1612q-47 0 -82 34t-35 81zM154 154h1536v1228h-1536v-1228zM307 264v363h203v-363h-203zM649 264v776h201v-776h-201zM993 264v592h203v-592h-203zM1333 264v922h203v-922 h-203z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t73 15.5h1153q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-1153q-39 0 -73.5 15.5t-60 41t-41 60.5t-15.5 76zM201 559q80 -111 198.5 -169t259.5 -58q96 0 184.5 26.5t160 73.5 t124 112.5t76.5 143.5q82 6 129 57q14 14 4 33q-8 18 -30 15h-4q23 23 30 45q8 20 -8 32q-14 14 -33 2q-8 -4 -29.5 -10t-45.5 -6q-4 0 -7.5 1t-7.5 1q0 2 -1 4t-1 4q-16 61 -57 110.5t-94 72.5q4 4 6 8t6 8q6 16 0 33q-2 6 -12.5 16t-34.5 8q-2 4 -6 8q-12 12 -25 9 q-25 -4 -49 -13l-2 2q-14 8 -31 -2q-59 -37 -98 -100t-68 -135q-35 31 -57 41q-61 35 -129 63.5t-154 61.5q-14 4 -24 -4q-10 -6 -15 -21q-2 -27 8.5 -58.5t38.5 -62.5q-25 -6 -20 -32q12 -68 68 -101l-13 -12q-14 -14 -4 -33q4 -12 26.5 -37.5t65.5 -38.5q-6 -12 -6 -22 t2 -14q6 -33 39 -50q-37 -25 -79 -34t-85 -5.5t-83 20.5t-70 46q-8 8 -19.5 8t-19.5 -8q-23 -18 -4 -39z" /> +<glyph unicode="" horiz-adv-x="1536" d="M2 193v1150q0 39 15.5 75t41 61.5t60.5 41t74 15.5h1153q80 0 136 -56.5t56 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-74 -15.5h-486v643h172q12 0 21.5 8t9.5 21l12 168q0 14 -8 24q-10 10 -23 10h-184v74q0 41 10.5 54.5t53.5 13.5q25 0 55.5 -4t58.5 -11 q6 0 13.5 1.5t11.5 5.5q10 6 14 22l23 162q4 29 -25 35q-90 25 -188 24q-301 0 -301 -293v-84h-103q-33 0 -32 -32v-168q0 -12 9 -21.5t23 -9.5h103v-643h-393q-39 0 -74 15.5t-60.5 41t-41 60.5t-15.5 76z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 160h1536v114h-1536v-114zM154 1145h1536v231h-957l-14 -92h-565v-139zM266 1341h316v93h-316v-93zM557 709q0 -76 28.5 -142.5t78 -116 t116 -78t142.5 -28.5t142 28.5t115.5 78t78 116t28.5 142.5t-28.5 142.5t-78 115.5t-116 77.5t-141.5 28.5q-76 0 -142.5 -28.5t-116 -77.5t-78 -115.5t-28.5 -142.5zM672 709q0 51 19.5 97t53 80t79.5 53t98 19q51 0 97 -19t80 -53t53 -80t19 -97t-19 -97.5t-53 -80 t-80 -53t-97 -19.5t-97.5 19.5t-80 53t-53 79.5t-19.5 98zM743 709q0 -16 12.5 -28.5t28.5 -12.5q18 0 30.5 12.5t12.5 28.5q0 41 28 67.5t67 26.5v2q18 0 30.5 12t12.5 29q0 18 -12.5 30.5t-30.5 12.5q-74 0 -126.5 -53.5t-52.5 -126.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 803.5q-6 111.5 30 230.5t113 228q78 109 179.5 180t209 102t213 17.5t191.5 -74.5q76 -53 121 -134t59 -176.5t-4 -199t-68 -203.5l414 -297l101 139l-105 74q-14 10 -17 27.5t7 32.5l51 69q10 14 26.5 17.5t32.5 -7.5l344 -247q16 -10 18.5 -27.5t-7.5 -32.5l-49 -69 q-10 -14 -27.5 -17.5t-32.5 6.5l-102 76l-101 -139l248 -178q43 -31 52.5 -83t-21.5 -95q-33 -43 -84 -51.5t-94 22.5l-803 573q-78 -80 -170 -130t-186.5 -68.5t-185.5 -2t-167 71.5q-86 61 -133 157.5t-53 208zM230.5 766q7.5 -41 30 -78t58.5 -63q37 -27 79 -36t83 -2 t78 29.5t64 59.5q39 53 41 114.5t-27 114.5q59 -10 116.5 11.5t96.5 74.5q27 37 36 79t2 84t-29.5 79t-59.5 63q-37 27 -79 36t-84 2t-79 -29.5t-63 -59.5q-37 -53 -39 -114.5t26 -114.5q-59 10 -116.5 -12.5t-96.5 -75.5q-27 -37 -36 -79t-1.5 -83z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 801v180q0 8 22.5 15.5t52 11.5t56.5 7t37 3q12 49 37 88q-23 31 -45.5 61.5t-46.5 59.5l-4 10q0 6 20.5 29.5t47 50t50 47t29.5 20.5q2 0 20.5 -13t41 -30.5t43 -34t24.5 -20.5q43 25 90 37q0 8 3 37t7 57.5t10.5 52t16.5 23.5h178q8 0 15.5 -23.5t11.5 -52t7 -57.5 t5 -37q45 -12 86 -35q31 23 62.5 45.5t60.5 46.5l8 4q4 0 27.5 -21.5t49.5 -47t47.5 -49t21.5 -29.5q0 -4 -13.5 -22.5t-30 -40t-32 -41t-19.5 -25.5q23 -39 39 -92q10 -2 37 -5t55.5 -8.5t51 -11.5t22.5 -14v-178q0 -10 -22.5 -16.5t-51 -11.5t-56.5 -7t-36 -4 q-14 -43 -37 -88q23 -31 44.5 -60.5t48.5 -58.5l2 -10q0 -6 -20.5 -30t-47.5 -50.5t-50.5 -47t-29.5 -20.5q-2 0 -20.5 13.5t-41 31t-42 32.5t-25.5 20q-45 -23 -88 -37q0 -10 -3 -37t-8 -56.5t-12.5 -52t-15.5 -22.5h-178q-8 0 -15.5 22.5t-11.5 52t-7 56t-3 37.5 q-45 12 -88 37q-31 -23 -61.5 -46.5t-59.5 -48.5l-10 -2q-4 0 -27.5 20.5t-49 47t-46 50.5t-20.5 30q0 2 13 20.5t29.5 40t31 41t18.5 25.5q-23 43 -39 94q-10 2 -37 5t-55.5 7t-51 10.5t-22.5 14.5zM420 889q0 -37 14.5 -70t39 -57.5t58 -38.5t70.5 -14t70 14t57.5 38.5 t38.5 57.5t14 70t-14 70.5t-38.5 58t-57.5 39t-70 14.5q-76 0 -129 -53t-53 -129zM1114 373q0 6 13.5 13t31 14.5t33.5 12.5t22 7q4 23 9.5 39t15.5 37q-4 4 -13 17t-18.5 27.5t-16.5 28t-7 17.5t18.5 23.5t43 41t47 40t28.5 24.5l8 4q4 0 16.5 -8.5t25.5 -19.5t25.5 -21.5 t16.5 -14.5q35 12 76 19q2 6 9 21.5t16.5 30.5t17.5 27.5t14 12.5q4 0 34 -7t63.5 -18.5t60.5 -23.5t27 -23q0 -23 -5.5 -47t-9.5 -47q16 -12 28.5 -26.5t22.5 -30.5q25 2 49.5 3t47.5 1q8 0 18 -26.5t17.5 -60.5t12.5 -64.5t5 -39.5q0 -6 -13.5 -13t-29.5 -13t-32.5 -12.5 t-22.5 -8.5q-9 -38 -23 -71q2 -6 11.5 -18.5t18.5 -27t16 -27.5t7 -18q0 -4 -18.5 -23.5t-43 -41t-47 -40.5t-28.5 -26l-8 -4q-4 0 -16.5 8.5t-25.5 19.5t-25.5 21.5t-16.5 14.5q-37 -12 -78 -19q-2 -6 -9 -21.5t-15.5 -30.5t-16.5 -27.5t-14 -12.5t-35 7.5t-62.5 18.5 t-60 23.5t-26.5 22.5q0 23 5 47.5t9 46.5q-16 12 -28.5 26.5t-22.5 30.5q-23 -2 -45.5 -3t-44.5 -1h-14.5t-8.5 11q-2 8 -8 35.5t-13.5 58t-12.5 55.5t-5 29zM1192 1264q0 6 12.5 12t29.5 10t33.5 6t22.5 4q10 33 31 64q-2 4 -8 17.5t-12.5 27.5t-11.5 25.5t-5 15.5 q0 6 21.5 22.5t49 35t51.5 32.5t28 14t13 -9t20.5 -21.5t20.5 -23.5t13 -15q14 4 28.5 6t29.5 0h14q2 4 11 16.5t18.5 25.5t17.5 23.5t12 10.5t30 -11.5t55.5 -26t53 -28.5t23.5 -20q0 -4 -4 -15.5t-8 -26t-8.5 -27.5t-6.5 -17q20 -25 39 -60q51 -4 74 -7t29 -18.5t5 -52 t3 -106.5q0 -6 -12.5 -12.5t-28.5 -10.5t-32.5 -6t-22.5 -4q-13 -36 -31 -63q2 -4 8 -16.5t13.5 -27t12.5 -26.5t5 -14q0 -6 -22.5 -23.5t-50 -36t-51.5 -33t-26 -14.5q-4 0 -14 9.5t-21.5 21.5t-20.5 23.5t-13 15.5q-14 -4 -28.5 -6t-29.5 0h-14q-4 -4 -12 -16.5t-17.5 -26 t-18.5 -23.5t-13 -10t-30 11t-54.5 25.5t-52 29t-23.5 20.5q0 2 3 14.5t8 26.5t9 27.5t6 17.5q-23 23 -38 59q-53 2 -75 5t-29 18.5t-5 52.5t-4 109zM1397 367q0 -49 34.5 -85t84.5 -36q49 0 84.5 34.5t35.5 86.5q0 49 -34.5 83.5t-85.5 34.5q-49 0 -84 -34.5t-35 -83.5z M1446 1206q0 -47 31.5 -78.5t76.5 -31.5q47 0 79 31.5t32 76.5q0 47 -31.5 79t-77.5 32q-47 0 -78.5 -32t-31.5 -77z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 997q0 119 61.5 222.5t167 180.5t246.5 122t303 45t303.5 -45t247 -122t166.5 -180.5t61 -222.5q0 -117 -61 -221t-166.5 -181t-247 -122t-303.5 -45q-37 0 -73.5 3t-71.5 7q-147 -113 -336 -155q-20 -4 -40.5 -7.5t-43.5 -7.5q-12 -2 -21.5 6.5t-13.5 18.5v2 q-4 12 3 19.5t18 17.5q18 18 34.5 36.5t29.5 42t24.5 55.5t19.5 79q-141 78 -224 195.5t-83 256.5zM649 258q6 4 13.5 8t13.5 8q51 -6 102 -6q197 0 369 56.5t298 155t198.5 231.5t72.5 286q0 41 -6 84q96 -78 151.5 -175t55.5 -208q0 -139 -83 -256.5t-224 -195.5 q8 -47 19 -79t25.5 -55.5t30 -42t33.5 -36.5q10 -10 17.5 -18.5t3.5 -18.5v-2q-2 -12 -12.5 -19.5t-22.5 -5.5q-23 4 -43.5 7.5t-40.5 7.5q-96 20 -180 60t-156 95q-35 -4 -71.5 -7t-73.5 -3q-141 0 -265 35t-225 94z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 193v643q0 49 35 83.5t84 34.5h348q27 14 43 30.5t35 39.5q16 20 32.5 38.5t34.5 37.5q33 35 76 68.5t66 74.5q16 29 23 64.5t12.5 71.5t12.5 70t23.5 60.5t47 42t81.5 15.5q61 0 111.5 -25.5t85.5 -68.5t54.5 -98.5t19.5 -112.5q0 -59 -17.5 -112.5t-42.5 -107.5 q35 2 70 4.5t70 2.5q55 0 107 -10.5t93 -35t66.5 -67.5t25.5 -109q0 -29 -5 -57t-15 -57q18 -43 18 -90q0 -78 -41 -142q10 -59 -7 -118.5t-58 -104.5q-4 -84 -45 -139.5t-102.5 -89t-135 -47t-143.5 -13.5q-72 0 -144.5 10.5t-142.5 28.5q-70 20 -139.5 43t-142.5 23h-375 q-49 0 -84 34.5t-35 84.5zM236 276q0 -35 22 -57t57 -22q33 0 56.5 22.5t23.5 56.5q0 33 -23.5 56.5t-56.5 23.5q-35 0 -57 -23.5t-22 -56.5zM492 193q70 0 136 -16.5t133.5 -35t141.5 -35t160 -16.5q45 0 98 6t99.5 26.5t78 56.5t31.5 95q0 10 -1 18.5t-3 18.5 q35 16 53.5 53t18.5 74q0 39 -21 68q60 49 60 123q0 23 -12.5 43t-26.5 35q16 29 28.5 57.5t12.5 62.5q0 35 -17.5 55.5t-43 31t-56.5 12.5t-57 2q-45 0 -90.5 -3t-90.5 -3q-31 0 -61.5 3t-58.5 15q0 41 16 78t35.5 74.5t35 78.5t15.5 91q0 33 -10.5 65.5t-29.5 59t-47 44 t-65 17.5h-11t-11 -2q-8 -4 -9 -8t-3 -13q-12 -59 -22.5 -123.5t-39.5 -117.5q-29 -51 -74 -88t-86 -78q-29 -31 -49 -56.5t-41.5 -48t-48.5 -42t-65 -35.5h-2v-643z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 309q0 29 5 57.5t15 57.5q-18 43 -18 90q0 78 41 141q-10 59 7.5 119t58.5 105q4 84 45 139t102 89t135 47t144 13q72 0 144.5 -10t141.5 -29q70 -20 139.5 -42.5t143.5 -22.5h375q49 0 83.5 -35t34.5 -84v-643q0 -49 -34.5 -84t-83.5 -35h-349q-27 -14 -43 -30.5 t-34 -38.5q-16 -20 -32.5 -39t-35.5 -37q-33 -35 -76 -69t-65 -74q-25 -43 -31 -99.5t-18.5 -106.5t-44 -84t-107.5 -34q-61 0 -111.5 25.5t-85 68.5t-54 98.5t-19.5 112.5q0 59 17.5 112.5t41.5 106.5q-35 -2 -69.5 -4t-69.5 -2q-55 0 -107.5 10.5t-93.5 35t-66.5 67.5 t-25.5 108zM119 309q0 -35 17.5 -55.5t43 -30.5t56 -12t57.5 -2q47 0 91 3t89 3q31 0 61.5 -3t59.5 -15q0 -41 -16.5 -78t-36 -75t-34.5 -79t-15 -90q0 -33 10 -65.5t29.5 -59t47 -43t64.5 -16.5q4 0 11.5 -1t11.5 1q8 4 9 8l3 12q12 59 22.5 124t38.5 118q29 51 74 88 t86 78q29 31 49.5 56.5t42 48t47 42t66.5 35.5h2v643q-72 0 -137.5 16.5t-133 36t-141 36t-159.5 16.5q-45 0 -98.5 -7.5t-99.5 -27t-78 -56t-32 -96.5q0 -10 1 -18t3 -19q-35 -16 -53 -53t-18 -74q0 -39 20 -67q-59 -49 -59 -123q0 -23 12 -43.5t27 -34.5 q-16 -29 -28.5 -57.5t-12.5 -63.5zM1202 860q0 -33 23.5 -56.5t56.5 -23.5q35 0 57.5 23.5t22.5 56.5q0 35 -22.5 57.5t-57.5 22.5q-33 0 -56.5 -22.5t-23.5 -57.5z" /> +<glyph unicode="" horiz-adv-x="837" d="M1 959.5q9 27.5 54 33.5l506 74l227 459q23 41 50 41v-1348l-453 -237q-41 -23 -64.5 -6.5t-15.5 63.5l86 504l-364 356q-35 33 -26 60.5z" /> +<glyph unicode="" horiz-adv-x="1802" d="M0 1073q0 137 43 231.5t112.5 153t156.5 84t177 25.5q63 0 125 -21.5t115 -53.5t97 -70t75 -68q31 31 76 68.5t98 69.5t113.5 53.5t126.5 21.5q88 0 175 -25.5t156.5 -84t112.5 -153t43 -231.5q0 -94 -34.5 -177t-76.5 -146.5t-79 -102.5t-39 -43l-615 -612 q-26 -23 -57 -23q-33 0 -55 23l-617 614q-4 2 -39.5 41t-77.5 102.5t-77 146.5t-35 177zM160 1073q0 -68 27.5 -131t61.5 -112.5t63 -79.5l28 -29l561 -559l561 559l29 29q29 30 62.5 79.5t61 113t27.5 130.5q0 104 -29.5 169t-77.5 101.5t-106.5 50t-113.5 13.5 q-53 0 -107.5 -25.5t-102.5 -61.5t-86 -74t-56 -60q-25 -31 -62 -31t-61 31q-18 23 -56.5 60.5t-86.5 73.5t-102.5 61.5t-105.5 25.5q-57 0 -115.5 -13.5t-106.5 -50t-77.5 -101t-29.5 -169.5z" /> +<glyph unicode="" horiz-adv-x="1689" d="M0 307v922q0 63 24.5 118.5t66.5 97.5t97.5 66.5t118.5 24.5h461v-193h-461q-47 0 -80.5 -33.5t-33.5 -80.5v-922q0 -47 33.5 -80.5t80.5 -33.5h461v-193h-461q-63 0 -118.5 24.5t-97.5 66.5t-66.5 97.5t-24.5 118.5zM508 584v368q0 33 22.5 54.5t55.5 21.5h428v293 q0 41 39 57q39 14 65 -14l553 -553q18 -18 18.5 -44t-18.5 -42l-553 -553q-18 -18 -43 -18q-10 0 -22 4q-39 16 -39 57v291h-428q-33 0 -55.5 22.5t-22.5 55.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-1150q-80 0 -136.5 56.5t-56.5 136.5zM207 1192q0 -57 40 -97t95 -40q57 0 97 40t40 97q0 55 -40 95t-97 40q-55 0 -95 -40 t-40 -95zM213 240q0 -10 9 -19.5t20 -9.5h200q12 0 20.5 9t8.5 20v706q0 29 -29 29h-200q-10 0 -19.5 -8.5t-9.5 -20.5v-706zM580 240q0 -10 9 -19.5t19 -9.5h201q12 0 20.5 9t8.5 20v383q0 68 26.5 113.5t102.5 45.5q59 0 79.5 -28.5t20.5 -81.5v-432q0 -10 8 -19.5 t21 -9.5h204q10 0 19.5 9t9.5 20v477q0 141 -81 208.5t-216 67.5q-55 0 -107.5 -15t-93.5 -56q0 16 -3 34.5t-25 18.5h-195q-10 0 -19 -8.5t-9 -20.5v-706z" /> +<glyph unicode="" horiz-adv-x="1916" d="M2 950q8 82 45 172t100 176t139 147.5t152 92.5t145.5 30t121.5 -40q53 -41 73.5 -107.5t12.5 -148.5l301 -225q111 63 215 73t180 -47q55 -41 80.5 -108.5t23.5 -150.5t-30.5 -177t-83.5 -188l428 -410q25 -25 4 -53q-12 -16 -33 -17q-10 0 -18 6l-517 293 q-74 -80 -155.5 -135t-161.5 -80.5t-151.5 -19.5t-127.5 47q-76 55 -95 158.5t9 228.5l-303 223q-76 -33 -145.5 -32t-122.5 40q-51 39 -72.5 104.5t-13.5 147.5zM171 858.5q3 -16.5 15 -27.5q21 -14 50 -14q31 0 65.5 17.5t71.5 46t72.5 66.5t66.5 79q10 14 7 30.5 t-15 26.5q-14 10 -30.5 8.5t-27.5 -16.5q-78 -104 -138 -143t-72 -35q-14 10 -31 7t-27 -15q-10 -14 -7 -30.5zM465 754l364 -271q8 -8 23 -8q20 0 33 17q10 14 8 29.5t-16 25.5l-347 258q-16 -14 -32.5 -27.5t-32.5 -23.5zM829.5 239.5q1.5 -16.5 16.5 -26.5q29 -23 69 -23 q41 0 88.5 21.5t95.5 57.5t94 84.5t87 101.5q10 12 8 28.5t-16 26.5q-12 10 -28.5 8t-26.5 -16q-51 -68 -102.5 -116t-95.5 -75.5t-78 -36t-48 4.5q-14 10 -29.5 7t-25.5 -18q-10 -12 -8.5 -28.5z" /> +<glyph unicode="" d="M0 307v922q0 63 24.5 119.5t65.5 97.5t97.5 65.5t119.5 24.5h582q-2 -14 -4 -27.5t-2 -29.5v-88q0 -23 6 -48h-582q-47 0 -80.5 -33.5t-33.5 -80.5v-922q0 -47 33.5 -80.5t80.5 -33.5h1075q47 0 81 33.5t34 80.5v340q41 -31 90 -49t103 -20v-271q0 -63 -25 -118.5 t-67 -97.5t-97 -66.5t-119 -24.5h-1075q-63 0 -119.5 24.5t-97.5 66.5t-65.5 97.5t-24.5 118.5zM692.5 522q-0.5 25 16.5 41l770 772h-269q-25 0 -41 16.5t-16 41.5v86q-2 23 15.5 40t41.5 17h576q23 0 40 -17.5t17 -39.5v-86v-490q0 -25 -17.5 -42t-39.5 -15h-86 q-25 0 -41.5 16.5t-16.5 40.5v268l-772 -770q-16 -16 -40.5 -16t-41.5 16l-79 80q-16 16 -16.5 41z" /> +<glyph unicode="" horiz-adv-x="1689" d="M0 584v368q0 33 22.5 54.5t55.5 21.5h428v293q0 41 37 57q39 14 67 -14l553 -553q16 -18 16.5 -44t-16.5 -42l-553 -553q-18 -18 -43 -18q-8 0 -24 4q-37 16 -37 57v291h-428q-33 0 -55.5 22.5t-22.5 55.5zM922 0v193h460q47 0 81 33.5t34 80.5v922q0 47 -33.5 80.5 t-81.5 33.5h-460v193h460q63 0 118.5 -24.5t97.5 -65.5t67 -97.5t25 -119.5v-922q0 -63 -25 -118.5t-67 -97.5t-97 -66.5t-119 -24.5h-460z" /> +<glyph unicode="" horiz-adv-x="1689" d="M0 1042v187q0 33 22.5 54.5t55.5 21.5h323q-2 12 -2 25v24v4q0 53 4.5 88t15.5 54.5t31.5 27.5t55.5 8h678q33 0 54.5 -8t32.5 -27.5t15 -54.5t4 -88v-25.5t-2 -27.5h324q33 0 55.5 -21.5t22.5 -54.5v-187q0 -63 -45.5 -130.5t-122 -128t-180 -106.5t-222.5 -65 q-51 -10 -91 -41.5t-40 -70.5q0 -35 17.5 -51.5t39 -31t40 -31.5t22.5 -52q4 -23 -2 -47q-4 -14 23.5 -23.5t67.5 -18.5t82 -22.5t64 -34.5q12 -10 19.5 -39.5t9.5 -64.5q2 -33 -6 -58.5t-29 -25.5h-985q-20 0 -28.5 25.5t-6.5 58.5q2 35 9.5 64.5t19.5 39.5q23 20 64 34 t80.5 23t68.5 18t25 24q-4 14 -4 25.5v21.5q2 35 21.5 52t42 31.5t39.5 31t17 51.5q0 39 -39.5 70.5t-93.5 41.5q-117 20 -220 66.5t-180 106t-122 127t-45 130.5zM154 1042q0 -20 23.5 -54t67.5 -70.5t106.5 -71.5t140.5 -60q-25 80 -44.5 175.5t-33.5 189.5h-260v-109z M1198 786q78 25 140.5 60t106.5 71.5t67.5 70.5t23.5 54v109h-262q-12 -94 -31.5 -189.5t-44.5 -175.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-337v25q0 39 2 97t-3.5 115.5t-24.5 104.5t-65 66q188 20 290 110t102 287q0 66 -22.5 128t-67.5 114q6 23 8 45t2 45 q0 41 -9.5 91t-31.5 85h-12q-43 2 -79 -8.5t-68 -27.5t-62.5 -37.5t-65.5 -39.5q-33 4 -65.5 6t-65.5 2t-65.5 -2t-65.5 -6q-35 18 -65.5 39t-62.5 38t-67.5 27.5t-79.5 8.5h-12q-23 -35 -32 -85t-9 -91q0 -23 2 -45.5t8 -44.5q-45 -51 -66.5 -114t-21.5 -128 q0 -193 95.5 -283t281.5 -112q-39 -16 -59.5 -50t-32.5 -75q-27 -8 -49.5 -15.5t-50.5 -7.5q-55 0 -88 28.5t-59.5 62.5t-56.5 62.5t-79 28.5q-4 0 -20.5 -2t-16.5 -12q0 -23 19.5 -32t31.5 -19q39 -31 56.5 -71t39 -76.5t62.5 -62.5t127 -26q35 0 80 11q0 -10 -1 -18.5 t-1 -16.5q0 -29 2 -59.5t-2 -59.5h-317q-80 0 -136.5 56.5t-56.5 136.5z" /> +<glyph unicode="" d="M0 39v614q0 16 11.5 27.5t27.5 11.5h229q16 0 27.5 -11t11.5 -28v-346h1229v346q0 16 11.5 27.5t27.5 11.5h229q16 0 27.5 -11t11.5 -28v-614q0 -39 -39 -39h-1765q-39 0 -39 39zM346 969.5q-6 15.5 16 38.5l504 505q23 23 55.5 23t55.5 -23l506 -505q23 -23 15.5 -38.5 t-38.5 -15.5h-307v-499q0 -33 -22.5 -55.5t-55.5 -22.5h-307q-33 0 -54.5 22.5t-21.5 55.5v499h-307q-33 0 -39 15.5z" /> +<glyph unicode="" horiz-adv-x="1609" d="M2 514q-2 35 1 68.5t7 74.5q4 35 7 72t10 66q14 66 30.5 128t44.5 117q20 41 45 81t54 79q10 14 21 24.5t24 20.5q23 23 45 45.5t49 42.5t57.5 36.5t65.5 33.5q33 16 67.5 29.5t71.5 27.5q70 29 156 51l2 2q45 12 85 17.5t79 5.5q59 0 112.5 -9t106.5 -20q41 -8 85 -15 t93 -7h2q29 0 62.5 5t66.5 5q25 0 45.5 -6t32.5 -25q23 -31 26 -71.5t-2 -75.5q-4 -35 -8 -71t2 -72q4 -23 11.5 -39.5t15.5 -36.5q8 -18 11 -40t7 -42q18 -104 15.5 -195.5t-23 -170.5t-56.5 -148.5t-81 -132.5q-37 -49 -79 -97.5t-92 -91.5t-110.5 -77.5t-134.5 -59.5 q-76 -27 -158.5 -34t-158.5 -11h-31q-104 0 -194.5 16.5t-192.5 16.5h-4q-35 0 -77 -10.5t-83 -12.5h-2q-37 0 -65.5 17.5t-42.5 42.5q-20 35 -17.5 72.5t12.5 68.5t10.5 66.5t-5 75.5t-12.5 81t-9 82z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 1169q0 55 14.5 114.5t43 114t68.5 98.5t93 69q12 -2 26.5 0t26.5 2q10 0 30 -1l40 -2t38.5 -4.5t27.5 -7.5q8 -6 15 -18t12 -26.5t9.5 -30t8.5 -25.5q6 -18 23.5 -65.5t35.5 -97.5t32.5 -93t14.5 -53q0 -37 -24.5 -68t-54 -57.5t-54.5 -50t-25 -45.5q0 -20 16.5 -49 t26.5 -46q84 -145 188.5 -248.5t252.5 -187.5q18 -10 45.5 -27.5t50.5 -17.5t52.5 32t60 70.5t62 70.5t58.5 32q10 0 51.5 -22.5t89.5 -50t93 -55.5t61 -36q16 -10 46 -25.5t40 -35.5q2 -6 2 -23q0 -16 -3 -36.5t-8 -43t-12 -43t-13 -35.5q-18 -39 -59.5 -71.5t-92.5 -55 t-104.5 -36t-92.5 -13.5q-80 0 -153.5 25.5t-145.5 54.5q-123 45 -232.5 118t-203.5 164t-175 196.5t-144 215.5q-25 41 -52.5 95.5t-51 112t-39 113.5t-15.5 105z" /> +<glyph unicode="" d="M0 307v922q0 63 24.5 118.5t66.5 97.5t97.5 66.5t118.5 24.5h1075q63 0 118.5 -24.5t97.5 -66.5t67 -97.5t25 -118.5v-922q0 -63 -25 -118.5t-67 -97.5t-97 -66.5t-119 -24.5h-1075q-63 0 -118.5 24.5t-97.5 66.5t-66.5 97.5t-24.5 118.5zM193 307q0 -47 33.5 -80.5 t80.5 -33.5h1075q47 0 81 33.5t34 80.5v922q0 47 -34 80.5t-81 33.5h-1075q-47 0 -80.5 -33.5t-33.5 -80.5v-922z" /> +<glyph unicode="" horiz-adv-x="1253" d="M0 84v1337q0 47 34 81t81 34h1024q47 0 80.5 -34t33.5 -81v-1337q0 -47 -33.5 -81t-80.5 -34t-80 33l-432 432l-432 -432q-33 -33 -80 -33t-81 34t-34 81zM158 186l469 471l469 -471v1194h-938v-1194z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-1150q-80 0 -136.5 56.5t-56.5 136.5zM193 1040q2 -35 15 -76.5t30 -78.5q16 -37 33.5 -72t33.5 -63q74 -129 176.5 -241 t225.5 -190q31 -18 67.5 -36.5t75.5 -32.5q41 -16 83 -31.5t81 -21.5q63 -10 112.5 2t90.5 34q27 12 57.5 33.5t44.5 52.5q4 8 9 27t9 38t6.5 37.5t-2.5 29.5q-4 12 -24.5 22t-36.5 21q-43 25 -71.5 41t-67.5 38q-16 10 -36 23.5t-38 13.5q-23 0 -46.5 -27.5t-37.5 -43.5 q-12 -14 -36.5 -43t-47.5 -31q-14 -2 -32.5 10t-37.5 23q-106 59 -182 136t-135 177q-10 16 -21.5 37.5t-7.5 38.5q2 18 24.5 33.5t35.5 29.5q14 16 31.5 37t19.5 43q4 16 -5 38t-16 42q-16 43 -28.5 76t-26.5 76q-6 16 -12 40.5t-21 30.5q-8 4 -26.5 7t-39 4.5t-37.5 0 t-24 -1.5h-8q-33 -16 -62.5 -45.5t-51 -69.5t-34 -88t-9.5 -100z" /> +<glyph unicode="" d="M0 399q0 20 13.5 33.5t33.5 13.5q14 0 31 -12q123 -109 287 -108q59 0 117.5 15t107.5 48q-29 16 -47.5 42t-18.5 58q0 23 8 39q-18 6 -44.5 20.5t-51 34t-41 43t-16.5 46.5q0 16 10 28.5t25 22.5q-41 25 -76 74t-35 98q0 33 33 43q-35 35 -55.5 81t-20.5 95 q0 23 9.5 41.5t37.5 18.5q18 0 84 -27t142.5 -60.5t146.5 -67.5t98 -48q25 -14 47.5 -31.5t44.5 -36.5q20 51 47 106.5t60 106.5t73 96t89 74q8 6 24 6q18 0 27 -8q16 6 41.5 13.5t42.5 7.5q29 0 43 -27q31 0 56.5 -15.5t25.5 -50.5q0 -31 -27 -53q96 -43 159.5 -125 t90.5 -182q6 -2 23 -2q47 0 88 16.5t57 16.5q18 0 31.5 -14.5t13.5 -32.5q0 -23 -19.5 -48.5t-33.5 -39.5q23 4 42 -7.5t19 -38.5q0 -29 -26.5 -50t-61.5 -37.5t-72.5 -25.5t-58.5 -11q-43 -139 -133 -246t-210 -178.5t-258 -109.5t-275 -38q-219 0 -415 93.5t-325 273.5 q-8 16 -8 26z" /> +<glyph unicode="" horiz-adv-x="790" d="M0 862v203q0 16 11.5 28.5t27.5 12.5h125v102q0 358 366 359q123 0 232 -31q33 -10 29 -43l-27 -199q-4 -16 -16 -26q-16 -10 -31 -6q-35 8 -72.5 12t-66.5 4q-53 0 -65.5 -16.5t-12.5 -67.5v-88h223q14 0 29 -14q10 -10 10 -29l-17 -205q0 -16 -11 -25.5t-27 -9.5h-207 v-784q0 -16 -11.5 -27.5t-27.5 -11.5h-258q-16 0 -27.5 11.5t-11.5 27.5v784h-125q-16 0 -27.5 11.5t-11.5 27.5z" /> +<glyph unicode="" horiz-adv-x="1847" d="M0 743q0 115 37 222.5t113 193.5l-3 4l3 2q-16 39 -21.5 81t-5.5 85q0 25 2 56.5t8 65.5t15.5 63.5t23.5 50.5h8q61 0 110.5 -12.5t94.5 -35t87 -52t91 -60.5q86 25 178.5 33t182.5 8t182 -8t180 -33q47 31 90 60.5t87 52t93.5 35t110.5 12.5h10q12 -20 21.5 -50 t15.5 -64t9 -65.5t3 -56.5q0 -43 -6 -85t-20 -81v-2l-2 -4q76 -86 112.5 -193.5t36.5 -222.5q0 -233 -66.5 -383.5t-188.5 -236.5t-292 -120t-376 -34q-207 0 -378 34t-292 120t-187.5 236.5t-66.5 383.5zM250 483q0 -145 64.5 -224t165 -116t219 -43t227.5 -6q74 0 152.5 2 t153.5 12.5t142.5 34t117.5 67.5t80 111.5t30 165.5q0 84 -27 147.5t-75 106.5t-114.5 64.5t-146.5 21.5q-78 0 -156.5 -7t-156.5 -7t-157 7t-157 7q-162 0 -262 -87t-100 -257zM494 524q0 66 26.5 113t65.5 47t66.5 -47t27.5 -113t-27.5 -112.5t-66.5 -46.5t-65.5 46.5 t-26.5 112.5zM795 258q-6 16 12 25q16 6 24 -13q27 -76 93 -75q31 0 56.5 20.5t35.5 54.5q8 20 26 13q16 -6 13 -25q-16 -47 -51 -75.5t-80 -28.5q-43 0 -78 28.5t-51 75.5zM864 369q0 -12 17.5 -21.5t42.5 -9.5t42 9.5t17 21.5t-17.5 21t-41.5 9q-25 0 -42.5 -9t-17.5 -21z M1167 524q0 -66 28 -112.5t67 -46.5t65.5 46.5t26.5 112.5t-27 113t-65 47q-39 0 -67 -47t-28 -113z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 117v626q0 49 35 84t82 35h665v156q0 113 43 213t118 175t175 118t213 43t213.5 -43t175 -118t117.5 -175.5t43 -212.5v-152q0 -33 -22.5 -55.5t-55.5 -22.5h-80q-33 0 -55 23t-22 55v152q0 66 -25 123t-67 99t-99 66.5t-123 24.5t-122 -24.5t-99 -66.5t-67.5 -99.5 t-24.5 -122.5v-156h119q47 0 81.5 -34.5t34.5 -84.5v-626q0 -47 -34.5 -82t-81.5 -35h-1020q-47 0 -82 35t-35 82zM494 164h266l-66 285q29 18 47.5 48.5t18.5 65.5q0 55 -39 95t-94 40t-94 -40t-39 -95q0 -35 18 -65.5t47 -46.5z" /> +<glyph unicode="" d="M0 154v1228q0 63 45 108.5t109 45.5h1536q63 0 108 -45t45 -109v-1228q0 -63 -45 -108.5t-108 -45.5h-1536q-63 0 -108.5 45t-45.5 109zM154 154h1536v575h-1536v-575zM154 1114h1536v268h-1536v-268zM260 250v115h307v-115h-307zM676 250v115h446v-115h-446z" /> +<glyph unicode="" horiz-adv-x="1566" d="M0 236q0 49 18.5 91t50 74.5t75 51t92.5 18.5t91 -18.5t74.5 -51t51 -74.5t18.5 -91t-18.5 -92.5t-51 -75t-74.5 -50t-91 -18.5t-92.5 18.5t-75 50t-50 74.5t-18.5 93zM0 819v158q0 35 25 57q23 23 53 23q2 0 3 -1t3 -1q193 -14 362.5 -95t301 -212.5t212 -301.5 t95.5 -362q4 -35 -21 -59q-23 -25 -57 -25h-158q-29 0 -51.5 20.5t-26.5 51.5q-12 133 -68.5 249.5t-145.5 205.5t-205.5 145.5t-249.5 68.5q-31 4 -51.5 27t-20.5 51zM0 1331v158q0 33 25 55q23 23 53 23h4q301 -16 565 -137t463 -320t319.5 -463t137.5 -565q4 -31 -23 -57 q-23 -25 -55 -25h-158q-31 0 -53.5 21.5t-24.5 52.5q-14 238 -110 446.5t-255 367.5t-368 255t-446 110q-31 2 -52.5 24.5t-21.5 53.5z" /> +<glyph unicode="" d="M0 193v382q0 23 6 45.5t12 45.5l238 727q20 63 75.5 103t121.5 40h938q66 0 121 -40t75 -103l238 -727q6 -23 12 -45.5t6 -45.5v-382q0 -41 -15.5 -76t-41 -60.5t-61 -41t-74.5 -15.5h-1458q-80 0 -136.5 56.5t-56.5 136.5zM154 193q0 -16 11 -27.5t28 -11.5h1458 q16 0 27.5 11t11.5 28v382q0 16 -11.5 27.5t-27.5 11.5h-1458q-16 0 -27.5 -11t-11.5 -28v-382zM213 768h1417l-188 578q-4 16 -19.5 26t-31.5 10h-938q-16 0 -31.5 -10t-20.5 -26zM1057 385q0 39 27.5 66.5t68.5 27.5q39 0 66.5 -27.5t27.5 -66.5q0 -41 -27.5 -68.5 t-66.5 -27.5q-41 0 -68.5 27.5t-27.5 68.5zM1364 385q0 39 27.5 66.5t68.5 27.5q39 0 66.5 -27.5t27.5 -66.5q0 -41 -27.5 -68.5t-66.5 -27.5q-41 0 -68.5 27.5t-27.5 68.5z" /> +<glyph unicode="" d="M0 754v229q0 63 45 108.5t109 45.5h499q115 0 245 33.5t255 90t235.5 128t186.5 147.5q63 0 108.5 -45t45.5 -109v-368q49 -12 81.5 -52.5t32.5 -93.5t-32.5 -93t-81.5 -52v-369q0 -63 -45.5 -108t-108.5 -45q-68 68 -164 132t-206.5 117.5t-227.5 91.5t-227 50 q-43 -12 -70 -40t-37 -62.5t-2 -70.5t37 -65q-25 -41 -23 -75.5t20.5 -66.5t49.5 -61.5t66 -58.5q-20 -43 -69.5 -65.5t-106 -25.5t-110.5 10.5t-85 43.5q-18 61 -40 125t-36 130.5t-14 138t24 151.5h-200q-63 0 -108.5 45t-45.5 109zM807 741q102 -16 206.5 -49t205 -78 t191.5 -99t165 -112v930q-76 -59 -166 -113.5t-190.5 -98.5t-205 -77.5t-206.5 -48.5v-254z" /> +<glyph unicode="" horiz-adv-x="1759" d="M0 317q104 78 172 172.5t109 201t60 224t28 240.5q6 84 56 159t126 131t163 89t167 33q74 0 163 -33t165.5 -88t129 -129t52.5 -158q6 -121 27.5 -239.5t63.5 -227t109.5 -204t167.5 -171.5q-4 -70 -56 -114.5t-120 -44.5h-469q-18 -82 -82.5 -135.5t-150.5 -53.5 t-151.5 53.5t-84.5 135.5h-469q-68 0 -120 45t-56 114zM238 317h1284q-78 86 -129.5 183.5t-84 202t-50 214t-25.5 222.5q-4 55 -41 104t-89 86t-111.5 57.5t-110.5 20.5q-47 0 -107.5 -21.5t-114 -56.5t-91.5 -83t-40 -99q-6 -111 -23.5 -222.5t-51 -218t-87 -205 t-128.5 -184.5zM713 209q0 -70 49 -119t119 -49q18 0 18 20t-18 21q-53 0 -91 37t-38 90q0 20 -19 20q-20 0 -20 -20z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 573q0 33 23 56l143 139l-143 139q-23 23 -23 56q0 25 16.5 45t40.5 26l195 49q-6 18 -15.5 48t-18.5 61t-16 59.5t-7 46.5q0 31 21.5 52.5t51.5 21.5q18 0 47 -7t60 -16.5t60.5 -18.5t47.5 -15l49 194q6 25 27 41.5t45 16.5q35 0 53 -23l142 -143l139 143q23 23 53 23 q27 0 47.5 -15.5t26.5 -42.5l49 -194q18 6 48 15t60.5 18.5t59.5 16.5t47 7q29 0 51.5 -21.5t22.5 -52.5q0 -18 -7 -46.5t-16.5 -59.5t-18.5 -60.5t-15 -48.5l194 -49q25 -6 41 -26.5t16 -44.5q0 -33 -22 -56l-145 -139l145 -139q23 -23 22 -56q0 -25 -16 -45t-41 -26 l-194 -49q6 -18 15 -48t18.5 -61t16.5 -59.5t7 -46.5q0 -29 -21.5 -51.5t-52.5 -22.5q-18 0 -47 7t-59.5 16.5t-60 18.5t-48.5 15l-49 -194q-6 -25 -26.5 -41.5t-45.5 -16.5q-33 0 -55 23l-139 143l-142 -143q-18 -23 -53 -23q-25 0 -45 16.5t-27 41.5l-49 194 q-18 -6 -47.5 -15t-60.5 -18.5t-59.5 -16.5t-47.5 -7q-31 0 -52 22.5t-21 51.5q0 18 7 46.5t16 59.5t18.5 60.5t15.5 48.5l-195 49q-25 6 -41 26.5t-16 44.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 193v643q0 49 35 83.5t84 34.5h348q27 14 42 30.5t34 39.5q16 20 33.5 38.5t33.5 37.5q35 35 77 68.5t65 74.5q25 43 29.5 99.5t17 106.5t44 84t109.5 34q61 0 111.5 -25.5t85.5 -68.5t54.5 -98.5t19.5 -112.5q0 -53 -15.5 -104.5t-38.5 -98.5h134l376 -2 q49 0 93.5 -17.5t76 -49.5t50 -75t18.5 -94q0 -49 -18.5 -92t-50 -74.5t-75.5 -49t-94 -17.5h-200q-10 -59 -39 -107q10 -59 -7.5 -117.5t-56.5 -103.5q0 -76 -28.5 -131t-75.5 -90t-109.5 -52.5t-132.5 -17.5q-78 0 -148.5 16.5t-136 36t-128 36t-123.5 16.5h-375 q-49 0 -84 34.5t-35 84.5zM233 276q0 -35 22.5 -57t57.5 -22q33 0 56.5 22.5t23.5 56.5q0 33 -23.5 56.5t-56.5 23.5q-35 0 -57.5 -23.5t-22.5 -56.5zM489 193q70 0 136.5 -16.5t133 -35t133 -35t138.5 -16.5q43 0 84 8t72 27.5t50 52.5t19 82q0 14 -1 26.5t-3 24.5 q35 16 53.5 53t18.5 74q0 39 -20 68q59 49 59 123q0 23 -12.5 43.5t-26.5 34.5q90 0 178 1t178 1q51 0 84 30.5t33 81.5q0 53 -32.5 85t-84.5 32q-172 0 -340 1t-338 1q0 41 16.5 78t35 74.5t34 78.5t15.5 91q0 33 -10.5 65.5t-29 59t-46 44t-64.5 17.5h-11t-11 -2 q-8 -4 -9 -8t-3 -13q-12 -59 -22.5 -123.5t-39.5 -117.5q-29 -51 -73 -88t-85 -78q-29 -31 -49 -56.5t-41.5 -48t-48.5 -42t-65 -35.5h-5v-643z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 821q0 51 18.5 94t50 75t75.5 49.5t94 17.5l376 2h134q-23 47 -38.5 98t-15.5 105q0 57 19.5 112.5t54.5 98.5t85 68.5t112 25.5q78 0 109.5 -34t43.5 -84t17.5 -106.5t29.5 -99.5q23 -41 65 -74.5t77 -68.5q16 -18 33.5 -37t33.5 -39q18 -23 33.5 -39t42.5 -31h348 q49 0 84 -34.5t35 -83.5v-643q0 -49 -35 -84t-84 -35h-375q-61 0 -123.5 -16.5t-128 -36t-136 -36t-148.5 -16.5q-70 0 -132.5 17.5t-109.5 52.5t-75.5 90t-28.5 131q-39 45 -56.5 103.5t-7.5 117.5q-29 47 -39 107h-200q-49 0 -93.5 17.5t-76 49t-50 74.5t-18.5 92z M121 821q0 -51 32.5 -81.5t84.5 -30.5q90 0 178 -1t178 -1q-14 -14 -26.5 -35t-12.5 -43q0 -74 59 -123q-20 -29 -20 -68q0 -37 18.5 -73.5t53.5 -53.5q-2 -12 -3 -24.5t-1 -26.5q0 -49 19 -82t50 -52.5t72 -27.5t84 -8q70 0 137.5 16.5t134 35t133 35t135.5 16.5v643h-4 q-41 16 -66.5 35.5t-47 42t-42 48t-48.5 56.5q-41 41 -85 78t-73 88q-29 53 -39 117.5t-23 123.5q-2 8 -3 12.5t-9 8.5q-4 2 -11 2h-11q-37 0 -65 -17.5t-46 -44t-28.5 -59.5t-10.5 -65q0 -49 15.5 -90.5t34 -79t34.5 -74.5t16 -78q-170 0 -337.5 -1t-339.5 -1 q-51 0 -84 -32t-33 -85zM1524 276q0 -35 23.5 -57t56.5 -22q35 0 57 22.5t22 56.5q0 33 -22.5 56.5t-56.5 23.5q-33 0 -56.5 -23.5t-23.5 -56.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 602q0 61 25.5 111.5t68.5 85.5t98.5 54t112.5 19q53 0 104.5 -15t98.5 -38v133l2 377q0 49 17.5 93t49 76t74.5 50.5t94 18.5q49 0 92.5 -18.5t75 -50.5t49 -76t17.5 -93v-201q59 -10 106 -38q59 10 118 -7.5t104 -56.5q76 0 131 -28.5t90 -76t52 -109.5t17 -132 q0 -78 -16 -148.5t-35.5 -136t-36 -128t-16.5 -124.5v-374q0 -49 -35 -84t-84 -35h-643q-49 0 -84 34.5t-35 84.5v348q-14 27 -30.5 42t-38.5 34q-20 16 -39 33.5t-37 33.5q-35 35 -68.5 77t-74.5 64q-43 25 -99.5 30t-106.5 17.5t-84 44t-34 109.5zM120 591q-1 -7 1 -11 q4 -8 8 -9.5t12 -3.5q59 -12 124 -22t118 -39q51 -29 88 -73t78 -85q31 -29 56.5 -49t48 -41.5t42 -48.5t35.5 -66v-4h643q0 70 16.5 136.5t35 133t35 133t16.5 138.5q0 43 -8.5 84t-28 71.5t-52 50t-81.5 19.5q-14 0 -26.5 -1t-25.5 -3q-16 35 -53 53.5t-74 18.5 q-39 0 -67 -21q-49 59 -123 60q-23 0 -43.5 -12.5t-34.5 -26.5q0 90 -1 178t-1 178q0 51 -30.5 84t-82.5 33q-53 0 -84.5 -33t-31.5 -84q0 -172 -1 -340t-1 -338q-41 0 -78 16.5t-75 35t-79 34t-90 15.5q-33 0 -65.5 -10.5t-59 -29t-43 -46t-16.5 -64.5q0 -4 -1 -11z M1210 -37q0 -35 23.5 -57.5t56.5 -22.5q35 0 57.5 22.5t22.5 57.5q0 33 -22.5 56.5t-57.5 23.5q-33 0 -56.5 -23.5t-23.5 -56.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 584q0 78 34 109.5t84 44t106.5 17.5t99.5 29q41 23 74.5 65t68.5 77q18 16 36.5 33.5t39.5 33.5q23 18 39 33.5t30 42.5v348q0 49 35 84t84 35h643q49 0 84 -35t35 -84v-375q0 -61 16.5 -123.5t36 -128t35.5 -136t16 -148.5q0 -70 -17 -132.5t-52 -109.5t-90.5 -75.5 t-130.5 -28.5q-45 -39 -103.5 -56.5t-118.5 -7.5q-47 -29 -106 -39v-200q0 -49 -17.5 -93.5t-49 -76t-74.5 -50t-93 -18.5q-51 0 -94 18.5t-74.5 50t-49 75.5t-17.5 94l-2 376v134q-47 -23 -98.5 -38.5t-104.5 -15.5q-57 0 -112.5 19.5t-98.5 54.5t-68.5 85t-25.5 112z M120 595q1 -7 1 -11q0 -37 16.5 -64.5t43 -46t59 -29t65.5 -10.5q49 0 90 15.5t79 34t75 35t78 16.5q0 -170 1 -338t1 -340q0 -51 31.5 -84t84.5 -33q51 0 82 32.5t31 84.5q0 90 1 178t1 178q14 -14 34.5 -26.5t43.5 -12.5q74 0 123 59q29 -20 67 -20q37 0 74 18.5t53 53.5 q12 -2 24.5 -3t27.5 -1q49 0 81.5 19t52 50t28 72t8.5 84q0 70 -16.5 137.5t-35 134t-35 133t-16.5 136.5h-643v-5q-16 -41 -35.5 -66.5t-42 -47t-48 -42t-56.5 -48.5q-41 -41 -78 -85t-88 -73q-53 -29 -117.5 -39t-124.5 -23l-12 -3t-8 -9q-2 -4 -1 -11zM1210 1223 q0 -33 23.5 -56.5t56.5 -23.5q35 0 57.5 23.5t22.5 56.5q0 35 -22.5 57.5t-57.5 22.5q-33 0 -56.5 -23t-23.5 -57z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -29 20 -49l490 -490q20 -20 48.5 -20t49.5 20l74 74q20 20 20 48 t-20 50l-246 246h612q29 0 49.5 19.5t20.5 48.5v104q0 29 -20.5 49.5t-49.5 20.5h-612l246 246q20 20 20 48.5t-20 49.5l-74 74q-20 20 -48 20t-50 -20l-490 -490q-20 -20 -20 -49z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 717q0 -29 20.5 -49.5t48.5 -20.5h613l-246 -246q-20 -20 -20.5 -48.5 t20.5 -49.5l74 -74q20 -20 47.5 -20t50.5 20l489 490q20 20 20.5 49t-20.5 49l-489 490q-20 20 -49 20t-49 -20l-74 -74q-20 -20 -20.5 -48t20.5 -50l246 -246h-613q-29 0 -49 -19.5t-20 -48.5v-104z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 768q0 -29 20 -49l74 -74q20 -20 48 -20t50 20l246 246v-612q0 -29 19.5 -49.5 t47.5 -20.5h105q29 0 49.5 20.5t20.5 49.5v612l245 -246q20 -20 49 -20t50 20l73 74q20 20 20.5 48t-20.5 50l-489 490q-20 20 -49 20t-49 -20l-490 -490q-20 -20 -20 -49z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5t-311.5 62.5t-254 171t-171 254t-62.5 311.5zM240 769q0 -28 20 -50l490 -490q20 -20 48.5 -20t49.5 20l489 490q20 20 20.5 49 t-20.5 49l-73 74q-20 20 -48 20t-51 -20l-245 -246v612q0 29 -19.5 49.5t-48.5 20.5h-104q-29 0 -49.5 -20.5t-20.5 -49.5v-612l-246 246q-20 20 -49 20t-49 -20l-74 -74q-20 -20 -20 -48z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 162 63.5 307.5t173 255t254 173t308.5 63.5q162 0 307 -63.5t254.5 -173t173 -255t63.5 -307.5q0 -164 -63.5 -308.5t-173 -254t-254.5 -173t-307 -63.5q-164 0 -308.5 63.5t-254 173t-173 254t-63.5 308.5zM186 1141h4q4 0 12.5 3t8.5 -9q0 -4 -3 -10.5 t11 -6.5q4 0 5 6t3 0l4 -16v-2q0 -4 -5 -6t-3 -6q4 -4 9.5 -4h9.5l4 2l2 2q0 -6 6 -8.5t10 -2.5h2q0 -2 -4 -4t0 -6l23 -4v-2l14 -29q0 -4 -3 -10t-7 -6t-3 3t1 7t-2 7t-15 3q-2 0 -5 -1t-1 -5l12 -26l3 -2l2 -2q-10 0 -13.5 -24t-3.5 -34l4 -18l2 -6v-4l-4 -19l29 -43h6 q2 -4 -2 -8t-2 -8l8 -8q0 -10 4 -14.5t13 -12.5q-2 -12 20.5 -22.5t28.5 -14.5q8 -23 20 -45.5t29 -38.5l2 -14q0 -2 -5 -4t-1 -6l16 -7q4 4 10.5 -8t10.5 -16l-2 -6l12 -17l6 -2l4 8q-4 10 -14 26.5t-20.5 32t-17.5 28t-7 14.5t-2 16t-4 17q6 -4 15 -7.5t15 -7.5 q4 -25 19.5 -40t27.5 -34q-4 -4 0 -5t7 -1q4 -4 4 -14q14 -16 36.5 -44t22.5 -44v-2l-4 -14q6 -16 20.5 -25.5t28.5 -15.5h4q20 -10 41 -21.5t43 -19.5l23 14q8 -2 17 -10t20.5 -18.5t27 -19.5t35.5 -11q14 10 14 -5v-4l25 -30l4 -15q10 -6 20.5 -15t16.5 -20h4 q10 0 16.5 -9t16.5 -9q6 0 6 6q0 14 2 18.5t5 5.5t6 1t3 4l-4 6q-4 0 -6 -5t-6 -1l-14 -8l-17 4l-24 41l8 57q2 4 -7.5 9t-5.5 10q-14 8 -34 8q-4 0 -20.5 -3t-16.5 3t3 16t7 21.5t6 19.5t2 10l13 27l-3 4l-16 4q-4 0 -13 -6t-17.5 -14.5t-14.5 -16.5t-6 -14l-45 -10 q-14 0 -23 12q-4 16 -17 35.5t-13 34.5q0 23 8 43t-6 43q2 0 5 2t1 6l12 14l2 2l2 -2q14 10 37 7t29 14l16 -13q4 -2 9 4.5t5 10.5l-8 4l39 10l2 -6l19 2l22 -14q4 0 8 5t9 1l20 -21q-4 -8 -1 -12t3 -8q0 -6 10.5 -27.5t18.5 -21.5q10 0 9 13t-1 17q0 20 -8 39t-15 39v6 q0 10 12.5 17.5t12.5 9.5q10 8 22.5 16t18.5 19l8 16v10h6t4 6q0 2 -3 3.5t-7 5.5q-4 2 -8 6l6 4q4 6 6 15t-2 16l15 8q-2 -6 4 -8t10 0l10 16q-4 10 -5 7t5 7q10 4 19.5 9.5t19.5 5.5q2 -2 4 -2q6 0 6 2q0 10 -4 12l12 25q14 0 21 12l18 2q6 2 6 8v2l35 10l4 11l-10 14 q2 0 2 4t-4 6t-7 4t-7 4l-4 -2l4 2h6h13t9 9q0 12 -14 12q-18 0 -41.5 -8.5t-32.5 -28.5l-14 -8l18 18l3 6q0 4 -9.5 5.5t-5.5 1.5q18 0 27.5 4t15.5 9t12.5 10t18.5 9q23 -4 44 -2t44 2q6 4 12 8.5t8 10.5l23 4q4 -4 12 2t8 10q0 10 -11 13t-11 14q0 2 1 6t-3 4 q-6 0 -19.5 -6t-19.5 -10q-4 -2 -5 1t-1 7l2 -2l8 4l23 8l4 4q0 6 -7.5 8t-11.5 2t-12 -3t-8 3l2 4q-12 10 -21.5 22.5t-9.5 16.5t1 8.5t-5 4.5q-4 0 -7 -1t-3 5q0 4 -5 17t-14 13l-8 -8q0 -6 -6 -9t-6 -7h-4l-21 -12q-2 4 -5 2t-7 -2h-2l-2 -2q8 0 7 9t-7 9l-16 -4 q-4 0 -4.5 1t2 4t4.5 7t0 8t-6.5 1t-4.5 1h6l5 5q2 2 -1.5 7t-5.5 7l-22 4l-14 12q-2 -2 -7.5 3t-9.5 7l-16 -6l-39 9q-4 0 -8 -2.5t-4 -6.5t4 -6t4 -6t3 -21.5t-5 -13.5l-10 -14q2 -4 7 -7t10 -7.5t9 -10.5t2 -18l-45 -31v-4q0 -8 4 -15t8 -18q10 -4 9.5 -7t-7 -6t-12.5 -6 t-6 -5t-4 -2h-8h-4q0 2 2 3t2 5l-19 12v-2l-8 16q4 10 1 14.5t-3 10.5q0 16 -14.5 16t-30.5 -4q4 2 -1 6.5t-7 4.5q-16 0 -38 14t-36 14q-6 0 -14 -2t-15 -4q4 2 5 10l-13 23l-2 2q-6 0 -14 -5t-8 9q0 2 2 4t0 4q-2 12 5 20.5t11 18.5q4 4 4 8t4 4q8 0 15.5 4.5t17.5 6.5 l2 6q0 4 -21.5 7t-21.5 7l2 2q20 -6 30.5 -7t17.5 2t16.5 8t29.5 14q0 4 -21.5 8t-27.5 8h12q4 0 10.5 -2t10.5 -4q0 -4 6 -5t10 -1l15 10v6l-4 6l22 4q-2 2 1 4t5 2q6 0 12.5 -6t8.5 -6l20 8q-2 2 5.5 3t4.5 6l-14 16q-2 0 -3 3t1 3q10 0 6 10q-8 4 -17 9.5t-20 5.5 q-4 0 -9 -2t-5 -7q0 -4 5 -4t7 -4q4 -4 -2 -4t-6 -2q-8 0 -15 -12t-18 -17q-4 0 -3 2.5t-1 4.5q-2 4 -7 5t-5 5t5 13t-13 9q-8 0 -11.5 -6t-7.5 -12l-22 25l-17 2q0 8 3.5 14t-9.5 16q-6 4 -11 7.5t-11 3.5q-2 0 -9.5 -4.5t-13.5 -8.5t-6 -8t10 -4h-2q-6 0 -6 -8 q0 -2 9.5 -5t13.5 -3q4 2 6 -1.5t6 -3.5l8 3v-5q-2 -2 -2 -4l2 -8l-20 -10q-2 -2 -4 -2t-5 -2q0 -6 5.5 -12.5t-11.5 -6.5l-6 4q0 8 -16.5 13.5t-40 7.5t-47 3t-35.5 1l-33 -10l8 -19q-4 0 -5 -3t3 -5q-4 4 -15 14.5t-15 10.5l-17 4q-35 -2 -78 -33t-84 -72t-75.5 -84 t-53.5 -71zM471 674h2h-2zM487 1413q14 2 25.5 8t24.5 6l6 -4q6 -2 11 -2t9 -4q8 2 17 -2l6 4v8l-2 5l6 -2q6 0 12 10l-2 4q-4 2 -10 4t-10 2t-16.5 -5t-28 -11.5t-29.5 -12.5t-19 -8zM918 86q0 -4 3 -9t3 -9q0 -5 -2 -7q115 20 217 74.5t182 140.5h-2q-8 -4 -14 0l-5 -4 l-12 4h-4l-4 -8l2 8q-6 8 -14 15l-4 2q-4 0 -4 -8q2 16 -8.5 27.5t-26.5 11.5q0 -2 -2 -2h-4l-5 4h7l4 12l-11 8l-2 -2q-14 2 -20 16l-4 2l-2 -2l-4 -2q-12 -4 -21 -8q-12 4 -18 10l-27 -2q0 6 -4 12.5t-12 6.5q-10 0 -20.5 -2t-14.5 -13q0 -4 2 -8t4 -6v-8l-2 -6l-4 -2h-2 l-6 16l6 10q-2 4 -2 10.5t-2 10.5l-2 4h-6l-15 -10h-8l-4 -4q-2 -2 -2 -4t-2 -3l-2 3h-8q-8 -8 -9 -19l3 -4l-9 -6l-2 -4l-6 -4q0 -2 -1 -2t-1 -2v-2v-9l-2 -2v4l-2 2q-2 12 -23 21h-6v-4q2 -6 8.5 -10.5t10.5 -8.5q-2 2 -5 1t-3 -3v-4l14 -20v-39l4 -10q-4 -16 -16 -27v2 l-4 -2l-3 -2l-2 -10l2 -2v-2l-4 4l-2 -13l-12 -4q-6 -4 -5 -11t-5 -11l2 -4l-6 -6q0 -4 -1 -6.5t-1 -6.5l2 -14l6 -4l4 4l2 6l2 -12q0 -4 -4 -8q-6 -4 -12 -9.5t-6 -15.5z" /> +<glyph unicode="" horiz-adv-x="1593" d="M8 242q0 33 12.5 62.5t34.5 51.5l609 609q-16 66 -17 122q0 98 38 186.5t102.5 153t151.5 102.5t185 38q94 0 181.5 -36t154.5 -101l-442 -162l-43 -236l184 -153l443 159q-10 -92 -50.5 -171t-104 -137t-145 -91t-174.5 -33q-61 0 -127 17l-606 -607q-47 -47 -112 -47 q-68 0 -115 47q-23 23 -50.5 47.5t-52 52t-41 58.5t-16.5 68zM203 242q0 -33 22.5 -56.5t57.5 -23.5q33 0 56 23.5t23 56.5q0 35 -23.5 57.5t-55.5 22.5q-35 0 -57.5 -23t-22.5 -57z" /> +<glyph unicode="" d="M0 76v217q0 31 22.5 53.5t53.5 22.5h1689q33 0 55.5 -22.5t22.5 -53.5v-217q0 -31 -22.5 -53.5t-55.5 -22.5h-1689q-31 0 -53.5 22.5t-22.5 53.5zM0 662v215q0 31 22.5 53t53.5 22h1689q33 0 55.5 -22.5t22.5 -52.5v-215q0 -33 -22.5 -55.5t-55.5 -22.5h-1689 q-31 0 -53.5 22.5t-22.5 55.5zM0 1243v217q0 31 22.5 53.5t53.5 22.5h1689q33 0 55.5 -22.5t22.5 -53.5v-217q0 -31 -22.5 -53.5t-55.5 -22.5h-1689q-31 0 -53.5 22.5t-22.5 53.5zM752 692h999v154h-999v-154zM1059 109h692v153h-692v-153zM1366 1274h385v153h-385v-153z " /> +<glyph unicode="" horiz-adv-x="1566" d="M0 1458q0 33 22.5 55.5t55.5 22.5h1411q33 0 55.5 -22.5t22.5 -55.5t-23 -55l-565 -565v-791q0 -33 -22.5 -55.5t-55.5 -22.5t-55 23l-236 235q-23 23 -22 56v555l-565 565q-23 23 -23 55z" /> +<glyph unicode="" d="M0 115v512h713q-2 -6 -2 -19v-153q0 -55 38.5 -95.5t96.5 -40.5h153q55 0 94.5 40t39.5 96v153q0 12 -3 19h713v-512q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM0 743v371q0 47 34 81t81 34h424v231q0 31 21.5 53.5t53.5 22.5h615q33 0 55.5 -22.5 t22.5 -53.5v-231h422q47 0 80.5 -34t33.5 -81v-371h-1843zM692 1229h461v153h-461v-153zM825 455v153q0 18 21 19h153q18 0 19 -19v-153q0 -18 -19 -19h-153q-20 0 -21 19z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 80v477q0 51 24.5 61.5t59.5 -24.5l162 -162l340 338l-338 338l-164 -164q-35 -35 -59.5 -25.5t-24.5 60.5v477q0 31 23 57q27 23 57 23h477q51 0 61.5 -24.5t-24.5 -59.5l-160 -158l338 -338l332 334l-162 162q-35 35 -24.5 59.5t61.5 24.5h477q33 0 55 -23 q25 -25 25 -57v-477q0 -51 -24.5 -61.5t-59.5 24.5l-162 162l-334 -334l338 -336l158 160q35 35 59.5 24.5t24.5 -61.5v-477q0 -35 -25 -55q-23 -25 -55 -25h-477q-51 0 -61.5 24.5t24.5 59.5l166 166l-336 336l-340 -340l162 -162q35 -35 24.5 -59.5t-61.5 -24.5h-477 q-31 0 -55 25q-25 20 -25 55z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 852v152q0 18 1 47.5t10 56.5t29.5 46.5t57.5 19.5q-45 29 -71.5 75.5t-26.5 104.5q0 43 16.5 82t46 68.5t68.5 46t82 16.5q45 0 84 -16.5t67.5 -46t46 -68.5t17.5 -82q0 -57 -27.5 -104t-72.5 -76q37 0 57.5 -19.5t29.5 -46.5t11 -56.5t2 -47.5v-152 q-14 -8 -23.5 -18.5t-27.5 -10.5h-328q-16 0 -26.5 10.5t-22.5 18.5zM158 57v387q0 78 45 138.5t98 109.5q10 10 25.5 21.5t33.5 15.5q18 6 41 7t45 5q61 10 130 19.5t135 19.5q-90 57 -144.5 151.5t-54.5 207.5q0 88 34 166.5t92 136t136 91.5t166 34t166 -34t136 -91.5 t92 -136t34 -166.5q0 -113 -54 -207t-145 -152q66 -10 134.5 -19t130.5 -20q23 -4 45 -5t41 -7q18 -4 33.5 -15.5t27.5 -21.5q66 -59 103.5 -116.5t37.5 -131.5v-387q-12 -6 -20 -13t-18.5 -14t-23.5 -14.5t-36 -15.5h-1368q-35 0 -54.5 22.5t-43.5 34.5zM1452 852v152 q0 18 2 47.5t11.5 56.5t30 46.5t56.5 19.5q-45 29 -72.5 75.5t-27.5 104.5q0 43 16.5 82t46 68.5t68.5 46t84 16.5q43 0 82 -16.5t68.5 -46t46 -68.5t16.5 -82q0 -57 -26.5 -104t-71.5 -76q37 0 56.5 -19.5t28.5 -46.5t11 -56.5t2 -47.5v-152q-12 -8 -22.5 -18.5 t-26.5 -10.5h-328q-18 0 -27.5 10.5t-23.5 18.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 1137q0 88 34 166.5t92 137t136 92.5t168 34q86 0 166 -33t139 -92q8 -8 21.5 -20.5t26 -25t21.5 -25.5t9 -25q0 -18 -12 -31q-6 -8 -25 -12q-47 -10 -88 -22.5t-86 -31.5q-4 -4 -16 -4t-25.5 10.5t-31 21.5t-42 21.5t-57.5 10.5q-35 0 -66.5 -13.5t-54 -37t-36 -54 t-13.5 -67.5q0 -41 17.5 -75t43 -63.5t56.5 -56.5t57 -53l180 -178q23 -25 54.5 -37t66.5 -12q43 0 73 16t46 16q12 0 39 -21.5t55.5 -49t50 -55t21.5 -42.5q0 -29 -36 -51t-83 -38.5t-94 -26t-72 -9.5q-86 0 -164.5 33t-140.5 92l-303 305q-61 59 -94 139.5t-33 166.5z M578 1010q0 29 35.5 51t82.5 38.5t94 26t72 9.5q86 0 166 -33t139 -92l303 -305q61 -59 94 -139.5t33 -166.5q0 -90 -33.5 -167.5t-92 -136t-137.5 -92.5t-167 -34q-86 0 -165.5 34t-139.5 93q-8 8 -21.5 19.5t-25.5 25t-21.5 26.5t-9.5 26q0 18 13 28q6 8 24 12 q47 10 88 22.5t86 33.5q12 4 17 4q12 0 25.5 -10.5t30.5 -21.5t41 -21.5t58 -10.5q72 0 121 49.5t49 120.5q0 41 -17 76t-44 63.5t-56.5 55.5t-56.5 53l-178 180q-53 49 -123 50q-43 0 -72.5 -17.5t-46.5 -17.5q-12 0 -38.5 21.5t-55 49t-50 56t-21.5 41.5z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 391q0 70 23.5 132.5t64.5 113.5t96.5 86t120.5 51q-33 41 -51 90t-18 105q0 66 24.5 123t66.5 99t99 66.5t123 24.5q113 0 196 -72q18 76 61.5 141.5t103 113.5t133 75t157.5 27q96 0 182 -37t149.5 -100.5t100.5 -149.5t37 -182q0 -57 -13 -109.5t-36 -99.5 q117 -59 188.5 -170t71.5 -248q0 -98 -37 -183t-101.5 -149.5t-149.5 -101.5t-183 -37h-1018q-80 0 -151.5 30.5t-125 84t-84 125t-30.5 151.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M16 72q-39 72 7 141l516 803v366h-78q-33 0 -54.5 22.5t-21.5 55.5q0 31 21.5 53.5t54.5 22.5h614q33 0 55.5 -22.5t22.5 -53.5q0 -33 -22.5 -55.5t-55.5 -22.5h-76v-368l514 -801q45 -70 7 -141q-18 -35 -51 -53.5t-70 -18.5h-1262q-37 0 -69.5 18.5t-51.5 53.5z M377 479h782l-313 490v413h-154v-411z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 180q0 86 44 168t113.5 146.5t154.5 103.5t169 39h13q18 25 57 60.5t63 58.5v12v12q-12 12 -28.5 27.5t-35 32t-33.5 32t-23 27.5q-84 -6 -172.5 33t-160 105.5t-116.5 150.5t-45 168q0 98 62.5 154.5t160.5 56.5q82 0 168 -39t155.5 -103.5t114 -146.5t44.5 -168 q0 -10 -1.5 -21.5t-3.5 -21.5l86 -76l34 17q34 18 90.5 46.5t129 65.5t148.5 76t150.5 77t135 69.5t102.5 52t52 26.5q16 8 37 9q14 0 39 -4.5t52.5 -9.5t53 -11t41.5 -8q29 -4 47.5 -26.5t18.5 -51.5q0 -43 -35 -66l-700 -485l700 -485q35 -23 35 -66q0 -29 -18.5 -51.5 t-47.5 -26.5q-16 -4 -41.5 -9t-53 -10t-52.5 -9.5t-39 -4.5q-10 0 -18.5 2.5t-18.5 6.5l-842 430l-86 -76q2 -10 3.5 -21.5t1.5 -21.5q0 -86 -44.5 -168t-114 -146.5t-155.5 -103.5t-168 -39q-98 0 -160.5 56.5t-62.5 154.5zM197 1358q0 -23 11 -49.5t27.5 -51t35 -45 t34.5 -34.5q35 -29 82 -54.5t94 -25.5q10 0 27 4v6q0 23 -11.5 48.5t-27.5 50t-34.5 46t-35.5 35.5q-35 29 -82 54.5t-94 25.5q-14 0 -26 -4v-6zM199 180q0 -6 2 -10q6 0 11 -1t11 -1q41 0 90 24.5t92.5 61.5t72 83t28.5 89q0 4 -1 5t-1 5q-8 2 -23 2q-41 0 -90 -24.5 t-92 -61.5t-71.5 -83t-28.5 -89zM586 616q53 -25 82 -69l108 96l45 -27l1016 703l-172 31l-975 -498l6 -127zM586 920l35 -35q12 29 36.5 41t53.5 26l-43 37q-29 -45 -82 -69zM741 768q0 35 23.5 57.5t58.5 22.5q33 0 55.5 -22.5t22.5 -57.5q0 -33 -22.5 -56.5t-57.5 -23.5 q-33 0 -56.5 23.5t-23.5 56.5zM903 575l762 -389l172 31l-727 502z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 385v707q0 33 16.5 70.5t38.5 60.5l258 258q23 23 61 39t70 16h463q31 0 53.5 -22.5t22.5 -53.5v-250q16 8 35.5 13.5t40.5 5.5h463q31 0 53 -22.5t22 -53.5v-1075q0 -33 -22.5 -55.5t-52.5 -22.5h-830q-33 0 -55.5 22.5t-22.5 55.5v229h-536q-33 0 -55.5 22.5 t-22.5 55.5zM154 461h460v293q0 45 10.5 87t45.5 74l159 162v305h-278v-321q0 -31 -22.5 -53.5t-53.5 -22.5h-321v-524zM186 1139h211v211zM768 154h676v921h-279v-321q0 -31 -22.5 -53.5t-52.5 -22.5h-322v-524zM801 831h211v211z" /> +<glyph unicode="" horiz-adv-x="1470" d="M0 1128q0 90 35 170t94 139.5t139 94.5t170 35q88 0 169 -34t143 -95l655 -656q12 -12 12 -28q0 -10 -11 -26.5t-26.5 -32t-32 -27t-26.5 -11.5q-16 0 -29 13l-655 655q-41 41 -92 61.5t-107 20.5q-57 0 -108 -21.5t-89 -59.5t-59.5 -89t-21.5 -109q0 -55 20.5 -106 t61.5 -92l745 -746q55 -55 133 -55q39 0 74 15.5t60.5 41t41 60.5t15.5 73q0 78 -56 134l-563 563q-29 29 -69 28q-41 0 -71 -29.5t-30 -70.5t29 -70l475 -475q12 -12 12 -28q0 -10 -11 -26.5t-26.5 -32t-32 -27t-26.5 -11.5q-16 0 -29 13l-475 473q-35 37 -55.5 85 t-20.5 99q0 53 20.5 100.5t55.5 83t83.5 56t101.5 20.5q51 0 99 -20.5t85 -55.5l561 -563q102 -102 102 -246q0 -74 -27.5 -137t-74.5 -110t-110.5 -75t-137.5 -28q-143 0 -246 103l-745 745q-61 61 -95 142t-34 169z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 154v1228q0 63 45 108.5t109 45.5h1075q63 0 108 -45l154 -154q45 -45 45 -108v-1075q0 -63 -45 -108.5t-109 -45.5h-1228q-63 0 -108.5 45t-45.5 109zM154 846h1075v536h-1075v-536zM846 922v385h190v-385h-190z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-1150q-80 0 -136.5 56.5t-56.5 136.5z" /> +<glyph unicode="" d="M0 78v215q0 31 22.5 53.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -53.5v-215q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM0 659v218q0 31 22.5 53t55.5 22h1689q31 0 53.5 -22.5t22.5 -52.5v-218q0 -31 -22.5 -53t-53.5 -22h-1689 q-33 0 -55.5 22.5t-22.5 52.5zM0 1243v217q0 31 22.5 53.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -53.5v-217q0 -31 -22.5 -53.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 53.5z" /> +<glyph unicode="" d="M0 211q0 39 15.5 75t41 61.5t60.5 40.5t76 15q39 0 73.5 -15t61 -40.5t42 -61.5t15.5 -75q0 -41 -15.5 -76t-42 -60.5t-61.5 -41t-73 -15.5q-80 0 -136.5 56.5t-56.5 136.5zM0 768q0 39 15.5 75t41 61.5t60.5 41t76 15.5q39 0 73.5 -15.5t61 -41t42 -61.5t15.5 -75 q0 -41 -15.5 -76t-42 -60.5t-61.5 -41t-73 -15.5q-41 0 -76 15.5t-60.5 41t-41 60.5t-15.5 76zM0 1325q0 39 15.5 75t41 61.5t60.5 41t76 15.5q39 0 73.5 -15.5t61 -41t42 -61.5t15.5 -75q0 -41 -15.5 -76t-42 -60.5t-61.5 -40.5t-73 -15q-41 0 -76 15t-60.5 40.5t-41 60.5 t-15.5 76zM522 154v114q0 33 22.5 55.5t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1167q-33 0 -55.5 21.5t-22.5 54.5zM522 711v114q0 33 22.5 55.5t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5 h-1167q-33 0 -55.5 22.5t-22.5 55.5zM522 1268v114q0 33 22.5 55.5t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1167q-33 0 -55.5 22.5t-22.5 55.5z" /> +<glyph unicode="" d="M0 594q0 55 30.5 90t68.5 61.5t69 50t31 56.5q0 29 -17.5 44t-46.5 15q-25 0 -44 -15t-34 -36l-53 37q23 39 58.5 60.5t78.5 21.5q55 0 96.5 -32t41.5 -91q0 -49 -30 -80t-67 -55.5t-67.5 -47t-30.5 -55.5h143v54h66v-115h-289q-2 10 -3 18.5t-1 18.5zM4 55l37 54 q6 -4 12 -10.5t15 -10.5q12 -8 29.5 -14t37.5 -6q35 0 55.5 18t20.5 47q0 31 -23.5 48.5t-58.5 17.5h-23l-16 37l90 108l6 6t7 6l4 6h-9q-4 -2 -14 -2h-92v-49h-66v111h263v-53l-97 -115q41 -6 74 -35t33 -82t-38 -95t-110 -42q-35 0 -60.5 9t-41.5 20q-25 14 -35 26z M14 1434l105 102h67v-360h93v-62h-259v62h93v258v8h-2q-10 -16 -22.5 -28.5t-29.5 -27.5zM522 154v114q0 33 22.5 55.5t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -54.5t-53.5 -21.5h-1167q-33 0 -55.5 21.5t-22.5 54.5zM522 711v114q0 33 22.5 55.5 t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1167q-33 0 -55.5 22.5t-22.5 55.5zM522 1268v114q0 33 22.5 55.5t55.5 22.5h1167q31 0 53.5 -22.5t22.5 -55.5v-114q0 -33 -22.5 -55.5t-53.5 -22.5h-1167q-33 0 -55.5 22.5t-22.5 55.5z " /> +<glyph unicode="" horiz-adv-x="1536" d="M0 692v152h1536v-152h-1536zM268 1114q0 117 44 202t118 141t170 83t203 27q88 0 176 -20.5t172 -49.5q20 -72 27.5 -156t7.5 -157q0 -10 -1 -22.5t-3 -24.5l-13 -2q-23 2 -50 2t-50 6q-18 59 -44 114.5t-62.5 98.5t-88 68.5t-122.5 25.5q-49 0 -94.5 -13.5t-80 -40 t-56 -66.5t-21.5 -93q0 -59 28.5 -102t75.5 -75t105.5 -54.5t118 -42t113.5 -39t93 -41.5h-696q-33 53 -51.5 110.5t-18.5 120.5zM285 310v63v45l110 2q31 -72 64 -134.5t76 -108.5t104 -71.5t152 -25.5q55 0 110 16.5t100.5 48t74 78.5t28.5 109q0 76 -49 128t-119 88 t-146.5 59.5t-132.5 45.5h617q8 -16 15 -38.5t10.5 -48t5.5 -50t2 -43.5q0 -131 -47.5 -226t-127 -156.5t-186 -91.5t-225.5 -30q-53 0 -93 4t-78 12.5t-78 20.5t-93 29q-12 4 -38 12t-36 16q-8 6 -12 50.5t-6 96.5t-2 100z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 0v154h1536v-154h-1536zM0 1532q10 2 19.5 2h19.5q72 0 140.5 -5t137.5 -5q100 0 200.5 2t199.5 6q-4 -16 -1 -36.5t3 -37.5v-8q-66 -10 -108 -7t-65.5 -5t-32.5 -39t-9 -109q0 -137 4 -272t10 -273q8 -147 88 -237t238 -90q125 0 207 29.5t131 88t69.5 146.5t20.5 203 q0 20 -2 65t-5 102.5t-8.5 119t-11.5 113.5t-13 89t-15 45q-33 33 -78 33q-6 0 -23.5 -1t-36 -1t-34 1t-21.5 3l2 82q82 4 163 -3t165 -7q39 0 77.5 5t79.5 5q4 0 9.5 -1t9.5 -1q2 -12 4 -24.5t2 -24.5t-4 -29q-23 -6 -52.5 -8t-58 -7t-48 -16.5t-19.5 -38.5q0 -14 1 -27 t3 -28q2 -6 5 -38.5t5 -79t4 -101.5t4 -103t3 -85t1 -50q0 -37 -2 -80.5t-8 -89t-17.5 -87.5t-29.5 -74q-41 -68 -104.5 -114t-137 -72.5t-153.5 -38t-154 -11.5q-72 0 -144.5 9.5t-140.5 35.5q-96 35 -151 88.5t-84 121t-36 148.5t-7 173v321v42t-1 62.5t-5 60.5t-10 38 q-12 16 -37 23.5t-53.5 10.5t-56.5 3t-44 4z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 154h460v335h-460v-335zM154 565h460v332h-460v-332zM154 975h460v332h-460v-332zM692 154h461v335h-461v-335zM692 565h461v332h-461v-332z M692 975h461v332h-461v-332zM1229 154h461v335h-461v-335zM1229 565h461v332h-461v-332zM1229 975h461v332h-461v-332z" /> +<glyph unicode="" d="M0 197q0 39 29 61l1579 1247q20 17 47 17q35 0 59 -29q10 -12 29.5 -33.5t38 -44t32 -47.5t13.5 -43q0 -39 -29 -61l-1579 -1248q-20 -16 -47 -16q-37 0 -59 29q-10 12 -30 33.5t-38 44t-31.5 47t-13.5 43.5zM256 922l117 36l37 117l34 -117l119 -36l-119 -37l-34 -117 l-37 117zM373 1229l235 71l72 236l72 -236l235 -71l-235 -72l-72 -235l-72 235zM942 1382l117 37l37 117l37 -117l116 -37l-116 -36l-37 -117l-37 117zM1286 1153l94 -121l369 291l-94 121zM1536 885l117 35l37 118l36 -118l117 -35l-117 -37l-36 -117l-37 117z" /> +<glyph unicode="" d="M0 358v404q0 25 6 51.5t17.5 53t27 51t31.5 40.5l223 222q16 16 41 31.5t51.5 26.5t53 17.5t51.5 6.5h80v198q0 31 22.5 53.5t52.5 22.5h1108q33 0 55.5 -22.5t22.5 -53.5v-1102q0 -31 -22.5 -53t-55.5 -22h-75v-7q0 -63 -25 -119.5t-65.5 -97.5t-97 -65.5t-120.5 -24.5 q-63 0 -119.5 24.5t-97.5 65.5t-65.5 97.5t-24.5 119.5v7h-307v-7q0 -63 -24.5 -119.5t-65.5 -97.5t-97.5 -65.5t-119.5 -24.5t-119.5 24.5t-97.5 65.5t-65.5 97.5t-24.5 119.5v7h-78q-31 0 -53.5 22.5t-22.5 52.5zM193 700h387v369h-78q-10 0 -31.5 -8t-30.5 -17l-223 -221 q-8 -8 -16 -29.5t-8 -31.5v-62zM346 276q0 -47 34 -80.5t81 -33.5t80.5 33.5t33.5 80.5t-33.5 81t-80.5 34t-81 -33.5t-34 -81.5zM1268 276q0 -47 33.5 -80.5t80.5 -33.5t81 33.5t34 80.5t-33.5 81t-81.5 34q-47 0 -80.5 -33.5t-33.5 -81.5z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 768q0 166 62.5 311.5t171 254t254 171t311.5 62.5t311 -62.5t254 -171t171 -254t62 -311.5t-62 -311.5t-171 -254t-254 -171t-311 -62.5q-59 0 -115.5 8.5t-112.5 24.5q23 35 46.5 80t35.5 92l11 41q6 23 16 66t29 112q23 -41 76 -70.5t118 -29.5q96 0 176 41 t136.5 114.5t88 174t31.5 217.5q0 88 -35.5 171t-102.5 147.5t-160 102t-208 37.5q-141 0 -248.5 -46t-179 -118.5t-107.5 -160.5t-36 -174q0 -104 40 -187.5t124 -117.5q31 -10 41 20q2 10 7 31.5t9 32.5q4 16 1 23t-13 22q-53 59 -53 155q0 76 27.5 145.5t78.5 122t124 84 t163 31.5q80 0 142.5 -23.5t105.5 -64.5t64.5 -96t21.5 -121q0 -86 -18.5 -164t-52.5 -136t-80 -92t-103 -34q-31 0 -57.5 12.5t-45 34t-25.5 49t1 60.5q18 76 44.5 151.5t26.5 130.5q0 49 -26.5 84t-81.5 35q-66 0 -110 -58.5t-44 -146.5q-2 -23 2 -45q2 -18 7.5 -40.5 t15.5 -41.5q-33 -141 -53.5 -225t-30.5 -131q-12 -55 -18 -80q-10 -45 -13.5 -93t-1.5 -89q-104 45 -192 119.5t-151.5 169t-98.5 207t-35 235.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 193v1150q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5t-61.5 -41t-75 -15.5h-829q27 39 59.5 95.5t49.5 117.5l10 41q6 25 17.5 68t29.5 112q20 -41 75.5 -70.5t121.5 -29.5q96 0 176 42t137 115.5 t89 175t32 220.5q0 90 -37 173t-103.5 147.5t-161 102.5t-208.5 38q-143 0 -251 -46.5t-180.5 -120t-108.5 -162.5t-36 -175q0 -104 40 -188.5t124 -118.5q14 -6 25.5 -1t15.5 21q4 10 8 31.5t8 34.5q8 23 -12 43q-23 29 -37 67.5t-14 91.5q0 76 27.5 145.5t79 123t124 85 t164.5 31.5q82 0 144.5 -23.5t105.5 -64.5t64.5 -97t21.5 -124q0 -86 -18.5 -164t-52 -136t-81 -93t-104.5 -35q-31 0 -57.5 12.5t-45 34t-25.5 49t1 60.5q18 78 45 154.5t27 131.5q0 49 -26.5 85t-82.5 36q-66 0 -110.5 -59t-44.5 -150q-2 -23 2 -45q4 -43 22 -84 q-35 -141 -54.5 -226t-29.5 -132q-12 -55 -18 -80q-16 -70 -14 -142.5t6 -117.5h-260q-80 0 -136.5 56.5t-56.5 136.5z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 342v438q55 -68 120.5 -96.5t153.5 -28.5h36t34 4q-8 -23 -14 -42t-6 -42q0 -41 18 -77.5t43 -67.5q-104 -4 -198.5 -22.5t-186.5 -65.5zM0 1214v129q0 39 15.5 75t41 61.5t60.5 41t76 15.5h1150q80 0 136.5 -56.5t56.5 -136.5v-1150q0 -41 -15.5 -76t-41 -60.5 t-61.5 -41t-75 -15.5h-583q23 39 36 81t13 89q0 70 -21.5 121t-54.5 90t-70.5 69.5t-70.5 57.5t-54.5 53.5t-21.5 59.5q0 49 34 81.5t76 72.5t75.5 99.5t33.5 163.5q0 78 -39 157t-106 122h131l133 76h-428q-129 0 -237.5 -36t-188.5 -143zM2 170q14 57 55 96t96.5 61.5 t115 33t110.5 10.5h31.5t31.5 -2q37 -27 78 -54.5t75 -61.5t55.5 -75t21.5 -92q0 -47 -19 -86h-460q-72 0 -126.5 49t-64.5 121zM102 1094q0 43 11.5 85t35 73.5t59.5 51t85 19.5q68 0 118 -41t81.5 -100.5t47 -128t15.5 -123.5q0 -45 -9 -84t-30.5 -68.5t-56.5 -47 t-84 -17.5q-66 0 -116 39t-85 96t-53.5 123.5t-18.5 122.5zM854 1051h223v-224h109v224h223v108h-223v225h-109v-225h-223v-108z" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 262q0 100 56.5 166t139.5 105.5t179 56t174 18.5q-23 29 -41.5 64t-18.5 76q0 23 6.5 41t14.5 40q-16 -4 -33.5 -4h-34.5q-68 0 -130 23.5t-108 66.5t-74 101.5t-28 130.5q0 68 25 128t67 109.5t98 84t120 49.5q88 18 176 18h411l-127 -74h-127q72 -43 107 -120.5 t35 -157.5q0 -94 -33 -149.5t-73 -94.5t-72.5 -70.5t-32.5 -78.5q0 -45 44 -82t97 -82t97 -107.5t44 -161.5q0 -102 -50 -175.5t-126.5 -121t-170 -70t-181.5 -22.5q-66 0 -141.5 15.5t-140 50.5t-106.5 90t-42 137zM174 307q0 -68 35 -116t87 -78.5t114.5 -44t117.5 -13.5 q51 0 104.5 11.5t96.5 38t69.5 68.5t26.5 103q0 49 -21.5 89.5t-53 72t-70.5 59t-76 52.5q-14 2 -29.5 3t-29.5 1q-57 0 -123 -12.5t-121 -41t-91 -75.5t-36 -117zM276 1247q0 -53 17.5 -117.5t51.5 -120t82.5 -92t111.5 -36.5q96 0 134 61.5t38 147.5q0 53 -14.5 118.5 t-45 124t-78 97t-112.5 38.5q-47 0 -82 -18t-57.5 -49t-34 -71t-11.5 -83zM999 1206h215v-215h107v215h215v105h-215v217h-107v-217h-215v-105z" /> +<glyph unicode="" d="M0 78v1382q0 31 22.5 53.5t55.5 22.5h1689q31 0 53.5 -22.5t22.5 -53.5v-1382q0 -33 -22.5 -55.5t-53.5 -22.5h-1689q-33 0 -55.5 22.5t-22.5 55.5zM154 461q63 0 119.5 -24.5t97.5 -65.5t65.5 -97.5t24.5 -119.5h921q0 63 25 119.5t65.5 97.5t97 65.5t120.5 24.5v614 q-63 0 -120 24.5t-97.5 65.5t-65.5 97.5t-25 119.5h-921q0 -63 -24.5 -119.5t-65.5 -97.5t-97.5 -65.5t-119.5 -24.5v-614zM539 768q0 104 29.5 195.5t81.5 159t122 106.5t150 39t149.5 -39t121.5 -106.5t83 -159t31 -195.5t-31 -195.5t-83 -159t-122 -106.5t-149 -39 q-80 0 -150 39t-122 106.5t-81.5 159t-29.5 195.5zM692 969l94 -99l39 35q8 6 11.5 13.5t7.5 11.5q4 2 8 10h2v-16q0 -8 -1 -16.5t-1 -18.5v-283h-139v-129h438v129h-141v529h-140z" /> +<glyph unicode="" horiz-adv-x="1228" d="M6 967q20 47 72 47h1075q50 0 70 -47t-17 -84l-536 -537q-27 -23 -56 -22q-29 0 -55 22l-536 537q-37 37 -17 84z" /> +<glyph unicode="" horiz-adv-x="1228" d="M6 371q-20 47 17 84l536 536q23 23 55 23q31 0 54 -23l538 -536q37 -37 17 -84t-70 -47h-1077q-50 0 -70 47z" /> +<glyph unicode="" horiz-adv-x="692" d="M0 768q0 33 23 55l538 537q16 16 38 21t44 -5q23 -8 36 -27.5t13 -41.5v-1076q0 -25 -13 -43t-36 -28t-44 -5t-38 21l-538 539q-23 23 -23 53z" /> +<glyph unicode="" horiz-adv-x="692" d="M0 231v1076q0 49 47 69t84 -16l539 -537q23 -23 22 -55q0 -31 -22 -53l-539 -539q-37 -37 -84 -16q-47 20 -47 71z" /> +<glyph unicode="" d="M0 115v1306q0 47 34 81t81 34h1614q47 0 80.5 -34t33.5 -81v-1306q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34t-34 81zM154 154h692v1153h-692v-1153zM999 154h691v1153h-691v-1153z" /> +<glyph unicode="" horiz-adv-x="1228" d="M0 584q0 31 22.5 54.5t53.5 23.5h1075q33 0 55.5 -23t22.5 -55q0 -33 -23 -56l-536 -536q-23 -23 -56 -23t-55 23l-536 536q-23 23 -23 56zM0 952q0 33 23 56l536 536q23 23 55 23q33 0 56 -23l536 -536q23 -23 23 -56q0 -31 -22.5 -54.5t-55.5 -23.5h-1075 q-31 0 -53.5 23t-22.5 55z" /> +<glyph unicode="" horiz-adv-x="1228" d="M0 584q0 31 22.5 54.5t53.5 23.5h1077q31 0 53.5 -23t22.5 -55q0 -33 -23 -56l-536 -536q-23 -23 -56 -23q-35 0 -53 23l-538 536q-23 23 -23 56z" /> +<glyph unicode="" horiz-adv-x="1228" d="M0 952q0 33 23 56l536 536q23 23 55 23q33 0 56 -23l536 -536q23 -23 23 -56q0 -31 -22.5 -54.5t-53.5 -23.5h-1077q-31 0 -53.5 23t-22.5 55z" /> +<glyph unicode="" d="M0 115v905q10 -10 20.5 -17.5t20.5 -15.5q129 -96 256 -191.5t252 -195.5q39 -31 82 -62.5t89 -57t96.5 -41t103.5 -15.5q55 0 105 15.5t96 40t89 56t84 64.5q125 100 252 195.5t256 191.5q10 8 20.5 15.5t20.5 17.5v-905q0 -47 -33.5 -81t-80.5 -34h-1614q-47 0 -81 34 t-34 81zM2 1434q0 41 36 71.5t77 30.5h1614q41 0 76.5 -30.5t35.5 -71.5q0 -31 -18.5 -69t-45 -75t-57 -67.5t-53.5 -48.5q-123 -92 -241.5 -182.5t-239.5 -182.5q-25 -18 -57.5 -45t-67.5 -51.5t-71 -42t-66 -17.5h-2h-2q-31 0 -67 17.5t-70.5 42t-67.5 51.5t-58 45 q-121 92 -239.5 182t-241.5 183q-23 18 -53.5 48.5t-57 67.5t-45 75t-18.5 69z" /> +<glyph unicode="" horiz-adv-x="1576" d="M0 1376q0 39 15.5 74t41 60.5t60 41t73.5 15.5t74 -15.5t61.5 -41t41 -60.5t14.5 -74t-14.5 -73.5t-41 -60t-61.5 -41t-74 -15.5t-73.5 15.5t-60 41t-41 60t-15.5 73.5zM10 41v991q0 16 12.5 28.5t28.5 12.5h281q16 0 28.5 -12t12.5 -29v-991q0 -16 -12.5 -28.5 t-28.5 -12.5h-281q-16 0 -28.5 12.5t-12.5 28.5zM524 41v991q0 16 12.5 28.5t28.5 12.5h273q33 0 37 -25.5t4 -48.5q57 53 129.5 76t150.5 23q190 0 304 -95.5t114 -293.5v-668q0 -16 -12.5 -28.5t-28.5 -12.5h-289q-16 0 -27.5 12.5t-11.5 28.5v604q0 76 -27.5 116 t-111.5 40q-53 0 -89 -17.5t-56.5 -47t-28.5 -70.5t-8 -88v-537q0 -16 -12.5 -28.5t-28.5 -12.5h-281q-16 0 -28.5 12.5t-12.5 28.5z" /> +<glyph unicode="" horiz-adv-x="1591" d="M0 662q-4 39 35 45l158 20q12 2 30 -8q12 -8 15 -27q14 -102 63 -191t123 -153.5t169 -101.5t200 -37q115 0 217 44t178 120t120 177t44 218t-44 218t-120 177t-178.5 120t-216.5 44q-94 0 -181.5 -30.5t-158.5 -88.5l159 -159q35 -35 24 -58.5t-58 -23.5h-463 q-16 0 -29.5 6t-24.5 16q-25 25 -24 55v463q0 47 24.5 57.5t59.5 -24.5l162 -162q109 92 238.5 140.5t271.5 48.5q166 0 311 -62.5t253.5 -171t171 -254t62.5 -311.5t-62.5 -311.5t-171 -254t-254 -171t-310.5 -62.5q-150 0 -285 52.5t-241.5 145.5t-176.5 220t-90 275z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 651q0 49 37 91t88 42q35 0 68 -22q-20 14 -21 37q0 8 8 24l334 502q14 20 37 21q16 0 24 -9q-25 18 -40 43t-15 56q0 49 38 90t87 41q33 0 62 -19l514 -342q23 -16 36 -40.5t13 -53.5q0 -25 -10.5 -48.5t-26.5 -41.5t-40 -29.5t-48 -11.5q-33 0 -68 23q20 -14 21 -37 q0 -14 -8 -25l-105 -158l227 -151q14 4 28 6t28 2q53 0 94 -29l475 -315q37 -27 58.5 -65.5t21.5 -84.5q0 -74 -52.5 -126t-125.5 -52q-55 0 -99 31l-475 315q-70 47 -75 131l-228 152l-106 -158q-14 -20 -37 -20q-14 0 -25 8q55 -39 56 -100q0 -25 -10.5 -47.5t-28 -41 t-40 -29.5t-46.5 -11q-35 0 -62 18l-514 342q-49 33 -49 92z" /> +<glyph unicode="" horiz-adv-x="1880" d="M0 627q0 195 73.5 365.5t201.5 298.5t299 202t366 74t366 -74t299 -202t201.5 -299t73.5 -365q0 -78 -14.5 -159t-42 -159t-68.5 -149.5t-92 -130.5q-23 -29 -59 -29h-1328q-37 0 -59 29q-53 59 -93 130.5t-67.5 149.5t-42 159t-14.5 159zM158 627q0 -49 33.5 -83 t82.5 -34t83 34t34 83t-34.5 82.5t-82.5 33.5q-49 0 -82.5 -33.5t-33.5 -82.5zM352 1098q0 -49 35 -84t82 -35q49 0 84 35t35 84t-35 82.5t-84 33.5q-47 0 -82 -33.5t-35 -82.5zM743 313q0 -82 57.5 -139t139.5 -57t139.5 57t57.5 139q0 49 -26 91t-67 71q4 14 15.5 55 t27 95.5t33 114t30.5 110.5t22.5 88t9.5 43q0 23 -17.5 40t-40.5 17q-20 0 -35.5 -12t-21.5 -31l-135 -487q-39 -2 -74 -17.5t-60.5 -42t-40 -61.5t-14.5 -74zM823 1292q0 -49 34 -82.5t83 -33.5t83 33.5t34 82.5t-34 83t-83 34t-83 -34t-34 -83zM1292 1098q0 -49 35 -83 t84 -34t83 34t34 83t-34 82.5t-83 33.5t-84 -33.5t-35 -82.5zM1489 627q0 -49 34 -83t83 -34t82.5 34t33.5 83t-33.5 82.5t-82.5 33.5t-83 -33.5t-34 -82.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 866q0 123 49 225.5t130 183.5t184.5 139.5t209.5 92.5q94 31 190.5 45.5t194.5 14.5t194.5 -14.5t190.5 -45.5q106 -35 210 -93t185 -139t130 -183.5t49 -225.5t-49 -225t-130 -183t-184.5 -140.5t-210.5 -92.5q-94 -31 -190 -45t-195 -14q-92 0 -180 12 q-92 -72 -195.5 -119t-215.5 -73q-27 -4 -55.5 -10.5t-53.5 -6.5q-16 0 -27.5 11.5t-11.5 27.5q0 12 8.5 21.5t16.5 17.5q31 33 53 60.5t37.5 57.5t25 64.5t17.5 79.5q-78 43 -145.5 100.5t-120 128t-82 153.5t-29.5 175zM160 866q0 -72 24.5 -133t65.5 -113.5t94 -95.5 t111 -75q27 -14 51 -27.5t49 -28.5q-10 -47 -17.5 -94t-19.5 -94q59 29 111.5 64.5t105.5 76.5q55 -8 110.5 -14t112.5 -6q156 0 308 43q84 25 171 68.5t158.5 107t116.5 143.5t45 178t-45 178t-116.5 143.5t-158.5 107.5t-171 69q-152 43 -308 43q-158 0 -307 -43 q-84 -25 -171 -69t-158.5 -107.5t-116.5 -143t-45 -178.5z" /> +<glyph unicode="" horiz-adv-x="1916" d="M0 997q0 98 40 182.5t106.5 151t150.5 113.5t170 76q76 25 154.5 36t156.5 11q80 0 159 -11.5t155 -35.5q86 -29 170 -76t149.5 -113.5t105 -150.5t39.5 -183q0 -98 -39.5 -182t-105 -149.5t-149.5 -113.5t-170 -77q-154 -47 -314 -47q-37 0 -72.5 3t-72.5 7 q-150 -115 -336 -155q-20 -4 -43 -9.5t-45 -5.5q-12 0 -21.5 9.5t-9.5 23.5q0 10 7.5 17.5l13.5 13.5q51 51 72.5 96t35.5 117q-63 35 -118.5 82t-97.5 104t-66.5 124t-24.5 142zM160 997q0 -72 30.5 -127t77.5 -99t104.5 -78t112.5 -64l-20 -111q33 18 63.5 42t59.5 46 q47 -6 95 -12t95 -6q135 0 264 41q61 18 124 52t114 81t84 105.5t33 129.5q0 72 -33 131.5t-84 105.5t-113.5 80t-124.5 52q-63 23 -129.5 32t-134.5 9q-66 0 -132 -9t-132 -32q-59 -18 -122.5 -52t-115 -80t-84 -105.5t-32.5 -131.5zM649 258l27 16q51 -6 102 -6 q197 0 381 62q111 37 211 99.5t177 147.5t123 190t46 230q0 41 -6 84q90 -72 148.5 -169t58.5 -214q0 -76 -24.5 -142.5t-66.5 -123.5t-97.5 -104t-118.5 -82q14 -72 35.5 -117t72.5 -96q6 -6 13.5 -14.5t7.5 -18.5q0 -16 -10.5 -23.5t-24.5 -7.5q-20 0 -43 5.5t-41 9.5 q-186 41 -336 155q-37 -4 -72.5 -7t-72.5 -3q-129 0 -254 32t-236 97z" /> +<glyph unicode="" horiz-adv-x="933" d="M0 557q0 8 2 10l209 932q4 16 16.5 26.5t28.5 10.5h344q18 0 31.5 -13.5t13.5 -33.5q0 -6 -1 -10t-3 -11l-180 -524q12 4 47 13.5t81 21.5t95 26.5t93 25.5t75 18.5t37 7.5q18 0 31.5 -13.5t13.5 -33.5q0 -12 -2 -19l-565 -1306q-10 -29 -43 -29q-18 0 -33 13.5t-15 33.5 q0 6 3 10l204 914q-12 -4 -48 -14.5t-82 -22.5t-97 -26.5t-95 -27t-76 -19.5t-38 -7q-20 0 -33.5 14.5t-13.5 32.5z" /> +<glyph unicode="" d="M0 78v383q0 33 22.5 55.5t55.5 22.5h133v153q0 55 40 94t95 39h518v174h-135q-31 0 -53.5 21.5t-22.5 54.5v385q0 31 22.5 53.5t53.5 22.5h385q31 0 53.5 -22.5t22.5 -53.5v-385q0 -33 -22.5 -54.5t-53.5 -21.5h-135v-174h518q55 0 95 -38.5t40 -94.5v-153h135 q31 0 53.5 -22.5t22.5 -55.5v-383q0 -33 -22.5 -55.5t-53.5 -22.5h-385q-33 0 -54 22.5t-21 55.5v383q0 33 21.5 55.5t53.5 22.5h136v153q0 18 -21 19h-518v-172h135q31 0 53.5 -22.5t22.5 -55.5v-383q0 -33 -22.5 -55.5t-53.5 -22.5h-385q-31 0 -53.5 22.5t-22.5 55.5v383 q0 33 22.5 55.5t53.5 22.5h135v172h-518q-20 0 -20 -19v-153h135q33 0 55.5 -22.5t22.5 -55.5v-383q0 -33 -22.5 -55.5t-55.5 -22.5h-383q-33 0 -55.5 22.5t-22.5 55.5z" /> +<glyph unicode="" horiz-adv-x="1884" d="M0 770q31 137 109.5 253t193.5 204t257 141t302 66v53q0 35 23.5 57.5t56.5 22.5q35 0 57.5 -22.5t22.5 -57.5v-53q160 -12 303 -65.5t258 -141.5t192.5 -204t108.5 -253q6 -29 -20 -43q-10 -6 -19 -6q-20 0 -28 12q-98 102 -218 103q-86 0 -163.5 -54.5t-130.5 -152.5 q-10 -23 -35 -23t-35 23q-82 150 -213 194v-526q0 -68 -23.5 -127t-63.5 -104.5t-93.5 -71t-114.5 -25.5t-115.5 25.5t-94.5 71t-63.5 104.5t-23.5 127q0 35 23.5 57.5t56.5 22.5q35 0 57.5 -22.5t22.5 -57.5q0 -70 40 -119t97 -49q55 0 95 49t40 119v526 q-131 -45 -213 -194q-10 -23 -35 -23t-34 23q-53 98 -130 152.5t-163 54.5q-121 0 -217 -103q-12 -12 -31 -12q-10 0 -19 6q-25 14 -20 43z" /> +<glyph unicode="" horiz-adv-x="1597" d="M0 385v1075q0 31 22.5 53.5t55.5 22.5h921q31 0 53.5 -22.5t22.5 -53.5v-231h33h61q25 0 55.5 -13.5t49.5 -29.5l280 -281q18 -18 30.5 -49t12.5 -55v-62v-678q0 -27 -17 -44t-44 -17h-860q-27 0 -44.5 17.5t-17.5 43.5v246h-536q-33 0 -55.5 22.5t-22.5 55.5zM276 1321 q0 -12 9.5 -21.5t21.5 -9.5h461q12 0 21.5 9.5t9.5 21.5v61q0 12 -9.5 21.5t-21.5 9.5h-461q-12 0 -21.5 -9t-9.5 -22v-61zM737 123h738v555h-367q-27 0 -44 17.5t-17 43.5v367h-310v-983zM1169 801h306q0 2 -3.5 8t-5.5 10l-280 279q0 4 -6 4q-2 0 -2 1t-2 1t-3.5 1t-3.5 1 v-305z" /> +<glyph unicode="" horiz-adv-x="2048" /> +<glyph unicode="" horiz-adv-x="1536" d="M0 57v379q0 39 20.5 87t54.5 90t74.5 73t81.5 35q-35 -59 -34 -129q0 -59 16 -124t59 -106q-18 -37 -18 -75q0 -72 50 -122t122 -50q37 0 68.5 13t55 36.5t37 55.5t13.5 67t-13.5 66.5t-37 55t-55 38t-68.5 14.5q-18 0 -36.5 -5t-35.5 -14q-12 12 -20 31t-13.5 40.5 t-7.5 41.5t-2 37q0 47 28 86t62 67l142 21q-92 57 -142.5 150.5t-50.5 201.5q0 86 33 162t90 133t133 90t162 33t162 -33t133 -90t90 -133t33 -162q0 -109 -50.5 -202t-142.5 -150l158 -23q18 -16 34.5 -36.5t16.5 -47.5q0 -45 -18 -77q-59 33 -121 32q-45 0 -87 -17 t-77 -46q-6 2 -11 3t-11 1q-47 0 -82 -34t-35 -81t34.5 -80.5t82.5 -33.5q47 0 80.5 33.5t33.5 80.5q0 8 -2 15.5t-4 13.5q16 12 35.5 20.5t42.5 8.5q55 0 95 -39t40 -94q0 -23 -8 -41.5t-19 -34.5q-25 10 -45 10q-47 0 -81.5 -34t-34.5 -81t34.5 -80.5t81.5 -33.5t81 33.5 t34 80.5l-2 4q74 74 74 177q0 74 -41 137q45 70 45 157q0 31 -12 62q41 -4 83 -34t74.5 -72t53 -90t20.5 -89v-379q-10 -6 -22.5 -15t-25.5 -18.5t-26.5 -16.5t-21.5 -7h-1344q-35 0 -52 20.5t-44 36.5zM369 287q0 25 17 41t40 16q25 0 41 -16t16 -41q0 -23 -16 -40.5 t-41 -17.5q-23 0 -40 17.5t-17 40.5z" /> +<glyph unicode="" horiz-adv-x="1597" /> +<glyph unicode="" horiz-adv-x="1597" /> +<glyph unicode="" horiz-adv-x="1597" /> +<glyph unicode="" horiz-adv-x="1597" /> +</font> +</defs></svg> \ No newline at end of file diff --git a/assets/font/fontawesome-webfont.ttf b/assets/font/fontawesome-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..c17e9f8d100d01a002029b5b93cc081062618bf3 Binary files /dev/null and b/assets/font/fontawesome-webfont.ttf differ diff --git a/assets/font/fontawesome-webfont.woff b/assets/font/fontawesome-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..09f2469a1f7a71acb4c4778842511d797d7f6ad8 Binary files /dev/null and b/assets/font/fontawesome-webfont.woff differ diff --git a/assets/font/index.html b/assets/font/index.html new file mode 100755 index 0000000000000000000000000000000000000000..60913d283b547abd5b4d2fbee33dfc39521c643b --- /dev/null +++ b/assets/font/index.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>MyApp</title> + <link rel="icon" type="image/png" href="/img/icon-16.png" /> + <link rel="apple-touch-icon-precomposed" sizes="57x57" href=".img/icon-57.png" /> + <link rel="apple-touch-icon-precomposed" sizes="72x72" href="/img/icon-72.png" /> + <link rel="apple-touch-icon-precomposed" sizes="114x114" href="/img/icon-114.png" /> + <link rel="stylesheet" href="/styles/css/app.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <script type="text/javascript" src="/js/jquery.min.js"></script> + <script type="text/javascript" src="/js/app.js"></script> + <!--[if lt IE 9]> + <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> + <![endif]--> + <!--[if IE]> + <style type="text/css"> + </style> + <![endif]--> +</head> +<body> +<div id="container"> +</div> +</body> +</html> diff --git a/client.inc.php b/client.inc.php index 91956c0d7dfc36e5ffb374f0812cea2f9693c6b9..ce36e7b68d886cb948ac4c9c4cd16555d104df08 100644 --- a/client.inc.php +++ b/client.inc.php @@ -50,6 +50,8 @@ if($_SESSION['_client']['userID'] && $_SESSION['_client']['key']) //is the user logged in? if($thisclient && $thisclient->getId() && $thisclient->isValid()){ $thisclient->refreshSession(); +} else { + $thisclient = null; } /******* CSRF Protectin *************/ diff --git a/css/font-awesome.css b/css/font-awesome.css new file mode 100644 index 0000000000000000000000000000000000000000..57f6f1fe71a64aa30de05f66643273c8fc5194f8 --- /dev/null +++ b/css/font-awesome.css @@ -0,0 +1,303 @@ +/* Font Awesome + the iconic font designed for use with Twitter Bootstrap + ------------------------------------------------------- + The full suite of pictographic icons, examples, and documentation + can be found at: http://fortawesome.github.com/Font-Awesome/ + + License + ------------------------------------------------------- + The Font Awesome webfont, CSS, and LESS files are licensed under CC BY 3.0: + http://creativecommons.org/licenses/by/3.0/ A mention of + 'Font Awesome - http://fortawesome.github.com/Font-Awesome' in human-readable + source code is considered acceptable attribution (most common on the web). + If human readable source code is not available to the end user, a mention in + an 'About' or 'Credits' screen is considered acceptable (most common in desktop + or mobile software). + + Contact + ------------------------------------------------------- + Email: dave@davegandy.com + Twitter: http://twitter.com/fortaweso_me + Work: http://lemonwi.se co-founder + + */ +@font-face { + font-family: "FontAwesome"; + src: url('../assets/font/fontawesome-webfont.eot'); + src: url('../assets/font/fontawesome-webfont.eot?#iefix') format('eot'), url('../assets/font/fontawesome-webfont.woff') format('woff'), url('../assets/font/fontawesome-webfont.ttf') format('truetype'), url('../assets/font/fontawesome-webfont.svg#FontAwesome') format('svg'); + font-weight: normal; + font-style: normal; +} + +/* Font Awesome styles + ------------------------------------------------------- */ +[class^="icon-"]:before, [class*=" icon-"]:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + display: inline-block; + text-decoration: inherit; +} +a [class^="icon-"], a [class*=" icon-"] { + display: inline-block; + text-decoration: inherit; +} +/* makes the font 33% larger relative to the icon container */ +.icon-large:before { + vertical-align: top; + font-size: 1.3333333333333333em; +} +.btn [class^="icon-"], .btn [class*=" icon-"] { + /* keeps button heights with and without icons the same */ + + line-height: .9em; +} +li [class^="icon-"], li [class*=" icon-"] { + display: inline-block; + width: 1.25em; + text-align: center; +} +li .icon-large[class^="icon-"], li .icon-large[class*=" icon-"] { + /* 1.5 increased font size for icon-large * 1.25 width */ + + width: 1.875em; +} +li[class^="icon-"], li[class*=" icon-"] { + margin-left: 0; + list-style-type: none; +} +li[class^="icon-"]:before, li[class*=" icon-"]:before { + text-indent: -2em; + text-align: center; +} +li[class^="icon-"].icon-large:before, li[class*=" icon-"].icon-large:before { + text-indent: -1.3333333333333333em; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.icon-glass:before { content: "\f000"; } +.icon-music:before { content: "\f001"; } +.icon-search:before { content: "\f002"; } +.icon-envelope:before { content: "\f003"; } +.icon-heart:before { content: "\f004"; } +.icon-star:before { content: "\f005"; } +.icon-star-empty:before { content: "\f006"; } +.icon-user:before { content: "\f007"; } +.icon-film:before { content: "\f008"; } +.icon-th-large:before { content: "\f009"; } +.icon-th:before { content: "\f00a"; } +.icon-th-list:before { content: "\f00b"; } +.icon-ok:before { content: "\f00c"; } +.icon-remove:before { content: "\f00d"; } +.icon-zoom-in:before { content: "\f00e"; } + +.icon-zoom-out:before { content: "\f010"; } +.icon-off:before { content: "\f011"; } +.icon-signal:before { content: "\f012"; } +.icon-cog:before { content: "\f013"; } +.icon-trash:before { content: "\f014"; } +.icon-home:before { content: "\f015"; } +.icon-file:before { content: "\f016"; } +.icon-time:before { content: "\f017"; } +.icon-road:before { content: "\f018"; } +.icon-download-alt:before { content: "\f019"; } +.icon-download:before { content: "\f01a"; } +.icon-upload:before { content: "\f01b"; } +.icon-inbox:before { content: "\f01c"; } +.icon-play-circle:before { content: "\f01d"; } +.icon-repeat:before { content: "\f01e"; } + +/* \f020 doesn't work in Safari. all shifted one down */ +.icon-refresh:before { content: "\f021"; } +.icon-list-alt:before { content: "\f022"; } +.icon-lock:before { content: "\f023"; } +.icon-flag:before { content: "\f024"; } +.icon-headphones:before { content: "\f025"; } +.icon-volume-off:before { content: "\f026"; } +.icon-volume-down:before { content: "\f027"; } +.icon-volume-up:before { content: "\f028"; } +.icon-qrcode:before { content: "\f029"; } +.icon-barcode:before { content: "\f02a"; } +.icon-tag:before { content: "\f02b"; } +.icon-tags:before { content: "\f02c"; } +.icon-book:before { content: "\f02d"; } +.icon-bookmark:before { content: "\f02e"; } +.icon-print:before { content: "\f02f"; } + +.icon-camera:before { content: "\f030"; } +.icon-font:before { content: "\f031"; } +.icon-bold:before { content: "\f032"; } +.icon-italic:before { content: "\f033"; } +.icon-text-height:before { content: "\f034"; } +.icon-text-width:before { content: "\f035"; } +.icon-align-left:before { content: "\f036"; } +.icon-align-center:before { content: "\f037"; } +.icon-align-right:before { content: "\f038"; } +.icon-align-justify:before { content: "\f039"; } +.icon-list:before { content: "\f03a"; } +.icon-indent-left:before { content: "\f03b"; } +.icon-indent-right:before { content: "\f03c"; } +.icon-facetime-video:before { content: "\f03d"; } +.icon-picture:before { content: "\f03e"; } + +.icon-pencil:before { content: "\f040"; } +.icon-map-marker:before { content: "\f041"; } +.icon-adjust:before { content: "\f042"; } +.icon-tint:before { content: "\f043"; } +.icon-edit:before { content: "\f044"; } +.icon-share:before { content: "\f045"; } +.icon-check:before { content: "\f046"; } +.icon-move:before { content: "\f047"; } +.icon-step-backward:before { content: "\f048"; } +.icon-fast-backward:before { content: "\f049"; } +.icon-backward:before { content: "\f04a"; } +.icon-play:before { content: "\f04b"; } +.icon-pause:before { content: "\f04c"; } +.icon-stop:before { content: "\f04d"; } +.icon-forward:before { content: "\f04e"; } + +.icon-fast-forward:before { content: "\f050"; } +.icon-step-forward:before { content: "\f051"; } +.icon-eject:before { content: "\f052"; } +.icon-chevron-left:before { content: "\f053"; } +.icon-chevron-right:before { content: "\f054"; } +.icon-plus-sign:before { content: "\f055"; } +.icon-minus-sign:before { content: "\f056"; } +.icon-remove-sign:before { content: "\f057"; } +.icon-ok-sign:before { content: "\f058"; } +.icon-question-sign:before { content: "\f059"; } +.icon-info-sign:before { content: "\f05a"; } +.icon-screenshot:before { content: "\f05b"; } +.icon-remove-circle:before { content: "\f05c"; } +.icon-ok-circle:before { content: "\f05d"; } +.icon-ban-circle:before { content: "\f05e"; } + +.icon-arrow-left:before { content: "\f060"; } +.icon-arrow-right:before { content: "\f061"; } +.icon-arrow-up:before { content: "\f062"; } +.icon-arrow-down:before { content: "\f063"; } +.icon-share-alt:before { content: "\f064"; } +.icon-resize-full:before { content: "\f065"; } +.icon-resize-small:before { content: "\f066"; } +.icon-plus:before { content: "\f067"; } +.icon-minus:before { content: "\f068"; } +.icon-asterisk:before { content: "\f069"; } +.icon-exclamation-sign:before { content: "\f06a"; } +.icon-gift:before { content: "\f06b"; } +.icon-leaf:before { content: "\f06c"; } +.icon-fire:before { content: "\f06d"; } +.icon-eye-open:before { content: "\f06e"; } + +.icon-eye-close:before { content: "\f070"; } +.icon-warning-sign:before { content: "\f071"; } +.icon-plane:before { content: "\f072"; } +.icon-calendar:before { content: "\f073"; } +.icon-random:before { content: "\f074"; } +.icon-comment:before { content: "\f075"; } +.icon-magnet:before { content: "\f076"; } +.icon-chevron-up:before { content: "\f077"; } +.icon-chevron-down:before { content: "\f078"; } +.icon-retweet:before { content: "\f079"; } +.icon-shopping-cart:before { content: "\f07a"; } +.icon-folder-close:before { content: "\f07b"; } +.icon-folder-open:before { content: "\f07c"; } +.icon-resize-vertical:before { content: "\f07d"; } +.icon-resize-horizontal:before { content: "\f07e"; } + +.icon-bar-chart:before { content: "\f080"; } +.icon-twitter-sign:before { content: "\f081"; } +.icon-facebook-sign:before { content: "\f082"; } +.icon-camera-retro:before { content: "\f083"; } +.icon-key:before { content: "\f084"; } +.icon-cogs:before { content: "\f085"; } +.icon-comments:before { content: "\f086"; } +.icon-thumbs-up:before { content: "\f087"; } +.icon-thumbs-down:before { content: "\f088"; } +.icon-star-half:before { content: "\f089"; } +.icon-heart-empty:before { content: "\f08a"; } +.icon-signout:before { content: "\f08b"; } +.icon-linkedin-sign:before { content: "\f08c"; } +.icon-pushpin:before { content: "\f08d"; } +.icon-external-link:before { content: "\f08e"; } + +.icon-signin:before { content: "\f090"; } +.icon-trophy:before { content: "\f091"; } +.icon-github-sign:before { content: "\f092"; } +.icon-upload-alt:before { content: "\f093"; } +.icon-lemon:before { content: "\f094"; } +.icon-phone:before { content: "\f095"; } +.icon-check-empty:before { content: "\f096"; } +.icon-bookmark-empty:before { content: "\f097"; } +.icon-phone-sign:before { content: "\f098"; } +.icon-twitter:before { content: "\f099"; } +.icon-facebook:before { content: "\f09a"; } +.icon-github:before { content: "\f09b"; } +.icon-unlock:before { content: "\f09c"; } +.icon-credit-card:before { content: "\f09d"; } +.icon-rss:before { content: "\f09e"; } + +.icon-hdd:before { content: "\f0a0"; } +.icon-bullhorn:before { content: "\f0a1"; } +.icon-bell:before { content: "\f0a2"; } +.icon-certificate:before { content: "\f0a3"; } +.icon-hand-right:before { content: "\f0a4"; } +.icon-hand-left:before { content: "\f0a5"; } +.icon-hand-up:before { content: "\f0a6"; } +.icon-hand-down:before { content: "\f0a7"; } +.icon-circle-arrow-left:before { content: "\f0a8"; } +.icon-circle-arrow-right:before { content: "\f0a9"; } +.icon-circle-arrow-up:before { content: "\f0aa"; } +.icon-circle-arrow-down:before { content: "\f0ab"; } +.icon-globe:before { content: "\f0ac"; } +.icon-wrench:before { content: "\f0ad"; } +.icon-tasks:before { content: "\f0ae"; } + +.icon-filter:before { content: "\f0b0"; } +.icon-briefcase:before { content: "\f0b1"; } +.icon-fullscreen:before { content: "\f0b2"; } + +.icon-group:before { content: "\f0c0"; } +.icon-link:before { content: "\f0c1"; } +.icon-cloud:before { content: "\f0c2"; } +.icon-beaker:before { content: "\f0c3"; } +.icon-cut:before { content: "\f0c4"; } +.icon-copy:before { content: "\f0c5"; } +.icon-paper-clip:before { content: "\f0c6"; } +.icon-save:before { content: "\f0c7"; } +.icon-sign-blank:before { content: "\f0c8"; } +.icon-reorder:before { content: "\f0c9"; } +.icon-list-ul:before { content: "\f0ca"; } +.icon-list-ol:before { content: "\f0cb"; } +.icon-strikethrough:before { content: "\f0cc"; } +.icon-underline:before { content: "\f0cd"; } +.icon-table:before { content: "\f0ce"; } + +.icon-magic:before { content: "\f0d0"; } +.icon-truck:before { content: "\f0d1"; } +.icon-pinterest:before { content: "\f0d2"; } +.icon-pinterest-sign:before { content: "\f0d3"; } +.icon-google-plus-sign:before { content: "\f0d4"; } +.icon-google-plus:before { content: "\f0d5"; } +.icon-money:before { content: "\f0d6"; } +.icon-caret-down:before { content: "\f0d7"; } +.icon-caret-up:before { content: "\f0d8"; } +.icon-caret-left:before { content: "\f0d9"; } +.icon-caret-right:before { content: "\f0da"; } +.icon-columns:before { content: "\f0db"; } +.icon-sort:before { content: "\f0dc"; } +.icon-sort-down:before { content: "\f0dd"; } +.icon-sort-up:before { content: "\f0de"; } + +.icon-envelope-alt:before { content: "\f0e0"; } +.icon-linkedin:before { content: "\f0e1"; } +.icon-undo:before { content: "\f0e2"; } +.icon-legal:before { content: "\f0e3"; } +.icon-dashboard:before { content: "\f0e4"; } +.icon-comment-alt:before { content: "\f0e5"; } +.icon-comments-alt:before { content: "\f0e6"; } +.icon-bolt:before { content: "\f0e7"; } +.icon-sitemap:before { content: "\f0e8"; } +.icon-umbrella:before { content: "\f0e9"; } +.icon-paste:before { content: "\f0ea"; } + +.icon-user-md:before { content: "\f200"; } diff --git a/include/.htaccess b/include/.htaccess new file mode 100644 index 0000000000000000000000000000000000000000..3a42882788717c9ed1d5c2fcc3277d21ec13152b --- /dev/null +++ b/include/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/include/ajax.content.php b/include/ajax.content.php index e97e96a5c3d1d894f5d7cacfad702c97ced25cf6..e93030a9ed9734c586ce3df32979b4163a80be36 100644 --- a/include/ajax.content.php +++ b/include/ajax.content.php @@ -37,42 +37,46 @@ class ContentAjaxAPI extends AjaxController { function ticket_variables() { $content=' -<div style="width:600px;"> +<div style="width:680px;"> <h2>Ticket Variables</h2> - Please note that non-base variables depends on the context of use. + Please note that non-base variables depends on the context of use. Visit osTicket Wiki for up to date documentation. <br/> <table width="100%" border="0" cellspacing=1 cellpadding=2> - <tr><td width="50%" valign="top"><b>Base Variables</b></td><td><b>Other Variables</b></td></tr> + <tr><td width="55%" valign="top"><b>Base Variables</b></td><td><b>Other Variables</b></td></tr> <tr> - <td width="50%" valign="top"> + <td width="55%" valign="top"> <table width="100%" border="0" cellspacing=1 cellpadding=1> - <tr><td width="100">%id</td><td>Ticket ID (internal ID)</td></tr> - <tr><td>%ticket</td><td>Ticket number (external ID)</td></tr> - <tr><td>%email</td><td>Email address</td></tr> - <tr><td>%name</td><td>Full name</td></tr> - <tr><td>%subject</td><td>Subject</td></tr> - <tr><td>%topic</td><td>Help topic (web only)</td></tr> - <tr><td>%phone</td><td>Phone number | ext</td></tr> - <tr><td>%status</td><td>Status</td></tr> - <tr><td>%priority</td><td>Priority</td></tr> - <tr><td>%dept</td><td>Department</td></tr> - <tr><td>%assigned</td><td>Assigned staff or team (if any)</td></tr> - <tr><td>%createdate</td><td>Date created</td></tr> - <tr><td>%duedate</td><td>Due date</td></tr> - <tr><td>%closedate</td><td>Date closed</td></tr> + <tr><td width="130">%{ticket.id}</td><td>Ticket ID (internal ID)</td></tr> + <tr><td>%{ticket.number}</td><td>Ticket number (external ID)</td></tr> + <tr><td>%{ticket.email}</td><td>Email address</td></tr> + <tr><td>%{ticket.name}</td><td>Full name</td></tr> + <tr><td>%{ticket.subject}</td><td>Subject</td></tr> + <tr><td>%{ticket.phone}</td><td>Phone number | ext</td></tr> + <tr><td>%{ticket.status}</td><td>Status</td></tr> + <tr><td>%{ticket.priority}</td><td>Priority</td></tr> + <tr><td>%{ticket.assigned}</td><td>Assigned staff and/or team</td></tr> + <tr><td>%{ticket.create_date}</td><td>Date created</td></tr> + <tr><td>%{ticket.due_date}</td><td>Due date</td></tr> + <tr><td>%{ticket.close_date}</td><td>Date closed</td></tr> + <tr><td>%{ticket.auth_token}</td><td>Auth. token used for auto-login</td></tr> + <tr><td>%{ticket.client_link}</td><td>Client\'s ticket view link</td></tr> + <tr><td>%{ticket.staff_link}</td><td>Staff\'s ticket view link</td></tr> + <tr><td colspan="2" style="padding:5px 0 5px 0;"><em>Expandable Variables (See Wiki)</em></td></tr> + <tr><td>%{ticket.<b>topic</b>}</td><td>Help topic</td></tr> + <tr><td>%{ticket.<b>dept</b>}</td><td>Department</td></tr> + <tr><td>%{ticket.<b>staff</b>}</td><td>Assigned/closing staff</td></tr> + <tr><td>%{ticket.<b>team</b>}</td><td>Assigned/closing team</td></tr> </table> </td> <td valign="top"> <table width="100%" border="0" cellspacing=1 cellpadding=1> - <tr><td width="100">%message</td><td>Message (incoming)</td></tr> - <tr><td>%response</td><td>Response (outgoing)</td></tr> - <tr><td>%note</td><td>Internal/transfer note</td></tr> - <tr><td>%staff</td><td>Staff\'s name (alert/notices)</td></tr> - <tr><td>%assignee</td><td>Assigned staff</td></tr> - <tr><td>%assigner</td><td>Staff assigning the ticket</td></tr> - <tr><td>%url</td><td>osTicket\'s base url (FQDN)</td></tr> - <tr><td>%auth</td><td>Client authentication token</td></tr> - <tr><td>%clientlink</td><td>Client auto-login link</td></tr> + <tr><td width="100">%{message}</td><td>Incoming message</td></tr> + <tr><td>%{response}</td><td>Outgoing response</td></tr> + <tr><td>%{comments}</td><td>Assign/transfer comments</td></tr> + <tr><td>%{note}</td><td>Internal note <em>(expandable)</em></td></tr> + <tr><td>%{assignee}</td><td>Assigned staff/team</td></tr> + <tr><td>%{assigner}</td><td>Staff assigning the ticket</td></tr> + <tr><td>%{url}</td><td>osTicket\'s base url (FQDN)</td></tr> </table> </td> </tr> diff --git a/include/ajax.kbase.php b/include/ajax.kbase.php index b51d8781dff46f94409c7b797e59973862d222a9..576b50585a0da4e8006a87b92026286bd9ce2219 100644 --- a/include/ajax.kbase.php +++ b/include/ajax.kbase.php @@ -36,7 +36,7 @@ class KbaseAjaxAPI extends AjaxController { case 'json': $resp['id'] = $canned->getId(); $resp['ticket'] = $canned->getTitle(); - $resp['response'] = $ticket?$ticket->replaceTemplateVars($canned->getResponse()):$canned->getResponse(); + $resp['response'] = $ticket?$ticket->replaceVars($canned->getResponse()):$canned->getResponse(); $resp['files'] = $canned->getAttachments(); @@ -44,7 +44,7 @@ class KbaseAjaxAPI extends AjaxController { break; case 'txt': default: - $response =$ticket?$ticket->replaceTemplateVars($canned->getResponse()):$canned->getResponse(); + $response =$ticket?$ticket->replaceVars($canned->getResponse()):$canned->getResponse(); } diff --git a/include/ajax.reports.php b/include/ajax.reports.php index 9d002eb52c698a6788e33635f5576c22560d93df..7fbe19c89c81605725572e45433ef20ea374fdcf 100644 --- a/include/ajax.reports.php +++ b/include/ajax.reports.php @@ -35,6 +35,8 @@ class OverviewReportAjaxAPI extends AjaxController { } function getData() { + global $thisstaff; + $start = $this->get('start', 'last month'); $stop = $this->get('stop', 'now'); if (substr($stop, 0, 1) == '+') @@ -46,29 +48,40 @@ class OverviewReportAjaxAPI extends AjaxController { "dept" => array( "table" => DEPT_TABLE, "pk" => "dept_id", - "sort" => 'ORDER BY dept_name', + "sort" => 'T1.dept_name', "fields" => 'T1.dept_name', - "headers" => array('Department') + "headers" => array('Department'), + "filter" => ('T1.dept_id IN ('.implode(',', db_input($thisstaff->getDepts())).')') ), "topic" => array( "table" => TOPIC_TABLE, "pk" => "topic_id", - "sort" => 'ORDER BY topic', - "fields" => "T1.topic", - "headers" => array('Help Topic') + "sort" => 'name', + "fields" => "CONCAT_WS(' / '," + ."(SELECT P.topic FROM ".TOPIC_TABLE." P WHERE P.topic_id = T1.topic_pid)," + ."T1.topic) as name ", + "headers" => array('Help Topic'), + "filter" => '1' ), - # XXX: This will be relative to permissions based on the - # logged-in-staff "staff" => array( "table" => STAFF_TABLE, "pk" => 'staff_id', - "sort" => 'ORDER BY T1.lastname, T1.firstname', - "fields" => "CONCAT_WS(' ', T1.firstname, T1.lastname)", - "headers" => array('Staff Member') + "sort" => 'name', + "fields" => "CONCAT_WS(' ', T1.firstname, T1.lastname) as name", + "headers" => array('Staff Member'), + "filter" => + ('T1.staff_id=S1.staff_id + AND + (T1.staff_id='.db_input($thisstaff->getDeptId()) + .(($depts=$thisstaff->getManagedDepartments())? + (' OR T1.staff_id IN('.implode(',', db_input($depts)).')'):'') + .')' + ) ) ); $group = $this->get('group', 'dept'); - $info = $groups[$group]; + $info = isset($groups[$group])?$groups[$group]:$groups['dept']; + # XXX: Die if $group not in $groups $queries=array( @@ -78,43 +91,62 @@ class OverviewReportAjaxAPI extends AjaxController { COUNT(*)-COUNT(NULLIF(A1.state, "overdue")) AS Overdue, COUNT(*)-COUNT(NULLIF(A1.state, "closed")) AS Closed, COUNT(*)-COUNT(NULLIF(A1.state, "reopened")) AS Reopened - FROM '.$info['table'].' T1 LEFT JOIN '.TICKET_TABLE.' T2 USING ('.$info['pk'].') - LEFT JOIN '.TICKET_EVENT_TABLE.' A1 USING (ticket_id) - WHERE A1.timestamp BETWEEN '.$start.' AND '.$stop.' - GROUP BY '.$info['fields'].' - ORDER BY '.$info['fields']), + FROM '.$info['table'].' T1 + LEFT JOIN '.TICKET_EVENT_TABLE.' A1 + ON (A1.'.$info['pk'].'=T1.'.$info['pk'].' + AND NOT annulled + AND (A1.timestamp BETWEEN '.$start.' AND '.$stop.')) + LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=A1.staff_id) + WHERE '.$info['filter'].' + GROUP BY T1.'.$info['pk'].' + ORDER BY '.$info['sort']), array(1, 'SELECT '.$info['fields'].', FORMAT(AVG(DATEDIFF(T2.closed, T2.created)),1) AS ServiceTime - FROM '.$info['table'].' T1 LEFT JOIN '.TICKET_TABLE.' T2 USING ('.$info['pk'].') - WHERE T2.closed BETWEEN '.$start.' AND '.$stop.' - GROUP BY '.$info['fields'].' - ORDER BY '.$info['fields']), + FROM '.$info['table'].' T1 + LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].') + LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=T2.staff_id) + WHERE '.$info['filter'].' AND T2.closed BETWEEN '.$start.' AND '.$stop.' + GROUP BY T1.'.$info['pk'].' + ORDER BY '.$info['sort']), array(1, 'SELECT '.$info['fields'].', FORMAT(AVG(DATEDIFF(B2.created, B1.created)),1) AS ResponseTime - FROM '.$info['table'].' T1 LEFT JOIN '.TICKET_TABLE.' T2 USING ('.$info['pk'].') + FROM '.$info['table'].' T1 + LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].') LEFT JOIN '.TICKET_THREAD_TABLE.' B2 ON (B2.ticket_id = T2.ticket_id AND B2.thread_type="R") LEFT JOIN '.TICKET_THREAD_TABLE.' B1 ON (B2.pid = B1.id) - WHERE B1.created BETWEEN '.$start.' AND '.$stop.' - GROUP BY '.$info['fields'].' - ORDER BY '.$info['fields']) + LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=B2.staff_id) + WHERE '.$info['filter'].' AND B1.created BETWEEN '.$start.' AND '.$stop.' + GROUP BY T1.'.$info['pk'].' + ORDER BY '.$info['sort']) ); $rows = array(); + $cols = 1; foreach ($queries as $q) { list($c, $sql) = $q; $res = db_query($sql); - $i = 0; + $cols += $c; while ($row = db_fetch_row($res)) { - if (count($rows) <= $i) - $rows[] = array_slice($row, 0, count($row) - $c); - $rows[$i] = array_merge($rows[$i], array_slice($row, -$c)); - $i++; + $found = false; + foreach ($rows as &$r) { + if ($r[0] == $row[0]) { + $r = array_merge($r, array_slice($row, -$c)); + $found = true; + break; + } + } + if (!$found) + $rows[] = array_merge(array($row[0]), array_slice($row, -$c)); } + # Make sure each row has the same number of items + foreach ($rows as &$r) + while (count($r) < $cols) + $r[] = null; } return array("columns" => array_merge($info['headers'], - array('Open','Assigned','Overdue','Closed','Reopened', + array('Opened','Assigned','Overdue','Closed','Reopened', 'Service Time','Response Time')), "data" => $rows); } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index b402d6efce3e5914ba2c3cec2471b2d534312cbc..be878efbe888278c0831e9badb87f197090084b1 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -181,12 +181,10 @@ class TicketsAjaxAPI extends AjaxController { function acquireLock($tid) { global $cfg,$thisstaff; - if(!$tid or !is_numeric($tid) or !$thisstaff or !$cfg) + if(!$tid or !is_numeric($tid) or !$thisstaff or !$cfg or !$cfg->getLockTime()) return 0; - - $ticket = Ticket::lookup($tid); - if(!$ticket || !$ticket->checkStaffAccess($thisstaff)) + if(!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffAccess($thisstaff)) return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>'Lock denied!')); //is the ticket already locked? @@ -198,17 +196,13 @@ class TicketsAjaxAPI extends AjaxController { //Ticket already locked by staff...try renewing it. $lock->renew(); //New clock baby! - - return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); + } elseif(!($lock=$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime()))) { + //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)); } - - //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)); + + return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); } function renewLock($tid, $id) { diff --git a/include/class.banlist.php b/include/class.banlist.php index 27729b44a42f45cb0d187f1e8f2a8b731991784e..c620b105033fbc1275b6f0cb82ee69bb2c5306d3 100644 --- a/include/class.banlist.php +++ b/include/class.banlist.php @@ -27,7 +27,7 @@ class Banlist { } function isbanned($email) { - return EmailFilter::isBanned($email); + return TicketFilter::isBanned($email); } function includes($email) { @@ -50,7 +50,7 @@ class Banlist { 'name' => 'SYSTEM BAN LIST', 'isactive' => 1, 'match_all_rules' => false, - 'reject_email' => true, + 'reject_ticket' => true, 'rules' => array(), 'notes' => 'Internal list for email banning. Do not remove' ), $errors); diff --git a/include/class.canned.php b/include/class.canned.php index 52a487529c368bc158777359260d36b808dc1d90..c84950e7d6d19b062dbb3a53c0ccfe95eab96c48 100644 --- a/include/class.canned.php +++ b/include/class.canned.php @@ -35,8 +35,10 @@ class Canned { .' count(filter.id) as filters ' .' FROM '.CANNED_TABLE.' canned ' .' LEFT JOIN '.CANNED_ATTACHMENT_TABLE.' attach ON (attach.canned_id=canned.canned_id) ' - .' LEFT JOIN '.EMAIL_FILTER_TABLE.' filter ON (canned.canned_id = filter.canned_response_id) ' - .' WHERE canned.canned_id='.db_input($id); + .' LEFT JOIN '.FILTER_TABLE.' filter ON (canned.canned_id = filter.canned_response_id) ' + .' WHERE canned.canned_id='.db_input($id) + .' GROUP BY canned.canned_id'; + if(!($res=db_query($sql)) || !db_num_rows($res)) return false; @@ -100,7 +102,7 @@ class Canned { if (!$this->_filters) { $this->_filters = array(); $res = db_query( - 'SELECT name FROM '.EMAIL_FILTER_TABLE + 'SELECT name FROM '.FILTER_TABLE .' WHERE canned_response_id = '.db_input($this->getId()) .' ORDER BY name'); while ($row = db_fetch_row($res)) diff --git a/include/class.client.php b/include/class.client.php index 5c5f3035ee6b485d6862950b35924f00e0f90b97..a650adbbe68140c3a0f8d807f46df2a9d7771460 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -4,7 +4,7 @@ Handles everything about client - NOTE: Please note that osTicket uses email address and ticket ID to authenticate the user*! + XXX: Please note that osTicket uses email address and ticket ID to authenticate the user*! Client is modeled on the info of the ticket used to login . Peter Rotich <peter@osticket.com> @@ -134,72 +134,92 @@ class Client { return (($id=self::getLastTicketIdByEmail($email)))?self::lookup($id, $email):null; } - /* static */ function tryLogin($ticketID, $email, $auth=null) { + /* static */ function login($ticketID, $email, $auth=null, &$errors=array()) { global $ost; + + $cfg = $ost->getConfig(); + $auth = trim($auth); + $email = trim($email); + $ticketID = trim($ticketID); # Only consider auth token for GET requests, and for GET requests, # REQUIRE the auth token - $auto_login = $_SERVER['REQUEST_METHOD'] == 'GET'; + $auto_login = ($_SERVER['REQUEST_METHOD'] == 'GET'); //Check time for last max failed login attempt strike. - $loginmsg='Invalid login'; - # XXX: SECURITY: Max attempts is enforced client-side via the PHP - # session cookie. if($_SESSION['_client']['laststrike']) { if((time()-$_SESSION['_client']['laststrike'])<$cfg->getClientLoginTimeout()) { - $loginmsg='Excessive failed login attempts'; - $errors['err']='You\'ve reached maximum failed login attempts allowed. Try again later or <a href="open.php">open a new ticket</a>'; - }else{ //Timeout is over. + $errors['login'] = 'Excessive failed login attempts'; + $errors['err'] = 'You\'ve reached maximum failed login attempts allowed. Try again later or <a href="open.php">open a new ticket</a>'; + $_SESSION['_client']['laststrike'] = time(); //renew the strike. + } else { //Timeout is over. //Reset the counter for next round of attempts after the timeout. - $_SESSION['_client']['laststrike']=null; - $_SESSION['_client']['strikes']=0; + $_SESSION['_client']['laststrike'] = null; + $_SESSION['_client']['strikes'] = 0; } } + + if($auto_login && !$auth) + $errors['login'] = 'Invalid method'; + elseif(!$ticketID || !Validator::is_email($email)) + $errors['login'] = 'Valid email and ticket number required'; + + //Bail out on error. + if($errors) return false; + //See if we can fetch local ticket id associated with the ID given - if(!$errors && is_numeric($ticketID) && Validator::is_email($email) && ($ticket=Ticket::lookupByExtId($ticketID))) { - //At this point we know the ticket is valid. + if(($ticket=Ticket::lookupByExtId($ticketID, $email)) && $ticket->getId()) { + //At this point we know the ticket ID is valid. //TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets?? //Check the email given. - # Require auth token for automatic logins - if (!$auto_login || $auth === $ticket->getAuthToken()) { - if($ticket->getId() && strcasecmp($ticket->getEmail(),$email)==0){ - //valid match...create session goodies for the client. - $user = new ClientSession($email,$ticket->getId()); - $_SESSION['_client']=array(); //clear. - $_SESSION['_client']['userID'] =$ticket->getEmail(); //Email - $_SESSION['_client']['key'] =$ticket->getExtId(); //Ticket ID --acts as password when used with email. See above. - $_SESSION['_client']['token'] =$user->getSessionToken(); - $_SESSION['TZ_OFFSET']=$cfg->getTZoffset(); - $_SESSION['TZ_DST']=$cfg->observeDaylightSaving(); - //Log login info... - $msg=sprintf("%s/%s logged in [%s]",$ticket->getEmail(),$ticket->getExtId(),$_SERVER['REMOTE_ADDR']); - $ost->logDebug('User login', $msg); - //Redirect tickets.php - session_write_close(); - session_regenerate_id(); - @header("Location: tickets.php?id=".$ticket->getExtId()); - require_once('tickets.php'); //Just incase. of header already sent error. - exit; - } - } + + # Require auth token for automatic logins (GET METHOD). + if (!strcasecmp($ticket->getEmail(), $email) && (!$auto_login || $auth === $ticket->getAuthToken())) { + + //valid match...create session goodies for the client. + $user = new ClientSession($email,$ticket->getExtId()); + $_SESSION['_client'] = array(); //clear. + $_SESSION['_client']['userID'] = $ticket->getEmail(); //Email + $_SESSION['_client']['key'] = $ticket->getExtId(); //Ticket ID --acts as password when used with email. See above. + $_SESSION['_client']['token'] = $user->getSessionToken(); + $_SESSION['TZ_OFFSET'] = $cfg->getTZoffset(); + $_SESSION['TZ_DST'] = $cfg->observeDaylightSaving(); + $user->refreshSession(); //set the hash. + //Log login info... + $msg=sprintf('%s/%s logged in [%s]', $ticket->getEmail(), $ticket->getExtId(), $_SERVER['REMOTE_ADDR']); + $ost->logDebug('User login', $msg); + + //Regenerate session ID. + $sid=session_id(); //Current session id. + session_regenerate_id(TRUE); //get new ID. + if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id()) + $session->destroy($sid); + + return $user; + + } } + //If we get to this point we know the login failed. + $errors['login'] = 'Invalid login'; $_SESSION['_client']['strikes']+=1; if(!$errors && $_SESSION['_client']['strikes']>$cfg->getClientMaxLogins()) { - $loginmsg='Access Denied'; - $errors['err']='Forgot your login info? Please <a href="open.php">open a new ticket</a>.'; - $_SESSION['_client']['laststrike']=time(); - $alert='Excessive login attempts by a client?'."\n". - 'Email: '.$_POST['lemail']."\n".'Ticket#: '.$_POST['lticket']."\n". + $errors['login'] = 'Access Denied'; + $errors['err'] = 'Forgot your login info? Please <a href="open.php">open a new ticket</a>.'; + $_SESSION['_client']['laststrike'] = time(); + $alert='Excessive login attempts by a user.'."\n". + 'Email: '.$email."\n".'Ticket#: '.$ticketID."\n". 'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n". 'Attempts #'.$_SESSION['_client']['strikes']; - $ost->logError('Excessive login attempts (client)', $alert, ($cfg->alertONLoginError())); - }elseif($_SESSION['_client']['strikes']%2==0){ //Log every other failed login attempt as a warning. - $alert='Email: '.$_POST['lemail']."\n".'Ticket #: '.$_POST['lticket']."\n".'IP: '.$_SERVER['REMOTE_ADDR']. + $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError())); + } elseif($_SESSION['_client']['strikes']%2==0) { //Log every other failed login attempt as a warning. + $alert='Email: '.$email."\n".'Ticket #: '.$ticketID."\n".'IP: '.$_SERVER['REMOTE_ADDR']. "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_client']['strikes']; - $ost->logWarning('Failed login attempt (client)', $alert); + $ost->logWarning('Failed login attempt (user)', $alert); } + + return false; } } ?> diff --git a/include/class.config.php b/include/class.config.php index cc9b497682097d0e622dbbd410b0a4f5b0fae601..3aff085f77be0820d61c980a3445a889e7dc19d5 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -18,9 +18,8 @@ require_once(INCLUDE_DIR.'class.email.php'); class Config { - var $id=0; - var $mysqltzoffset=0; - var $config=array(); + var $id = 0; + var $config = array(); var $defaultDept; //Default Department var $defaultSLA; //Default SLA @@ -33,18 +32,27 @@ class Config { } function load($id=0) { + if(!$id && !($id=$this->getId())) return false; - $sql='SELECT * FROM '.CONFIG_TABLE + $sql='SELECT *, (TIME_TO_SEC(TIMEDIFF(NOW(), UTC_TIMESTAMP()))/3600) as db_tz_offset ' + .' FROM '.CONFIG_TABLE .' WHERE id='.db_input($id); + if(!($res=db_query($sql)) || !db_num_rows($res)) return false; - $this->config=db_fetch_array($res); - $this->id=$this->config['id']; - $this->setMysqlTZ(db_timezone()); + $this->config = db_fetch_array($res); + $this->id = $this->config['id']; + + //Get the default time zone + // We can't JOIN timezone table above due to upgrade support. + if($this->config['default_timezone_id']) + $this->config['tz_offset'] = Timezone::getOffsetById($this->config['default_timezone_id']); + else + $this->config['tz_offset'] = 0; return true; } @@ -92,17 +100,9 @@ class Config { return null; } - - 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; + function getDBTZoffset() { + return $this->config['db_tz_offset']; } /* Date & Time Formats */ @@ -149,7 +149,7 @@ class Config { } function getTZOffset() { - return $this->config['timezone_offset']; + return $this->config['tz_offset']; } function getPageSize() { @@ -543,35 +543,32 @@ class Config { return $this->config['upload_dir']; } - function updateSettings($vars,&$errors) { + 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); + case 'system': + return $this->updateSystemSettings($vars, $errors); break; case 'tickets': - return $this->updateTicketsSetting($vars,$errors); + return $this->updateTicketsSettings($vars, $errors); break; case 'emails': - return $this->updateEmailsSetting($vars,$errors); + return $this->updateEmailsSettings($vars, $errors); break; case 'attachments': return $this->updateAttachmentsSetting($vars,$errors); break; - case 'autoresponders': - return $this->updateAutoresponderSetting($vars,$errors); + case 'autoresp': + return $this->updateAutoresponderSettings($vars, $errors); break; case 'alerts': - return $this->updateAlertsSetting($vars,$errors); + return $this->updateAlertsSettings($vars, $errors); break; case 'kb': - return $this->updateKBSetting($vars,$errors); + return $this->updateKBSettings($vars, $errors); break; default: $errors['err']='Unknown setting option. Get technical support.'; @@ -580,7 +577,7 @@ class Config { return false; } - function updateGeneralSetting($vars, &$errors) { + function updateSystemSettings($vars, &$errors) { $f=array(); $f['helpdesk_url']=array('type'=>'string', 'required'=>1, 'error'=>'Helpdesk URl required'); @@ -589,6 +586,13 @@ class Config { $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'); + //Date & Time Options + $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; @@ -610,26 +614,6 @@ class Config { .',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='.db_input($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']) @@ -641,7 +625,7 @@ class Config { return (db_query($sql)); } - function updateTicketsSetting($vars,&$errors) { + function updateTicketsSettings($vars, &$errors) { $f=array(); @@ -658,7 +642,30 @@ class Config { $errors['enable_captcha']='PNG support required for Image Captcha'; } - if(!Validator::process($f,$vars,$errors) || $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. Must be less than '.$maxfileuploads; + + if(!$vars['max_staff_file_uploads'] || $vars['max_staff_file_uploads']>$maxfileuploads) + $errors['max_staff_file_uploads']='Invalid selection. Must be less than '.$maxfileuploads; + } + + + + if(!Validator::process($f, $vars, $errors) || $errors) return false; $sql='UPDATE '.CONFIG_TABLE.' SET updated=NOW() ' @@ -676,14 +683,24 @@ class Config { .',show_answered_tickets='.db_input(isset($vars['show_answered_tickets'])?1:0) .',show_related_tickets='.db_input(isset($vars['show_related_tickets'])?1:0) .',show_notes_inline='.db_input(isset($vars['show_notes_inline'])?1:0) + .',clickable_urls='.db_input(isset($vars['clickable_urls'])?1:0) .',hide_staff_name='.db_input(isset($vars['hide_staff_name'])?1:0) + .',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 updateEmailsSetting($vars,&$errors) { + function updateEmailsSettings($vars, &$errors) { $f=array(); $f['default_email_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default email required'); @@ -704,6 +721,7 @@ class Config { .',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_auto_cron='.db_input(isset($vars['enable_auto_cron'])?1:0) .',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) @@ -756,7 +774,7 @@ class Config { return (db_query($sql)); } - function updateAutoresponderSetting($vars,&$errors) { + function updateAutoresponderSettings($vars, &$errors) { if($errors) return false; @@ -771,7 +789,7 @@ class Config { } - function updateKBSetting($vars,&$errors) { + function updateKBSettings($vars, &$errors) { if($errors) return false; @@ -784,7 +802,7 @@ class Config { } - function updateAlertsSetting($vars,&$errors) { + function updateAlertsSettings($vars, &$errors) { if($vars['ticket_alert_active'] @@ -831,7 +849,6 @@ class Config { 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) diff --git a/include/class.dept.php b/include/class.dept.php index 5516646129b348de5eba208d005c8ab87d2f4145..24ec3489db61ecc0aa5b41f392842d6ebb172e18 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -24,7 +24,7 @@ class Dept { var $ht; - function Dept($id){ + function Dept($id) { $this->id=0; $this->load($id); } @@ -55,24 +55,28 @@ class Dept { return true; } - function reload(){ + function reload() { return $this->load(); } - function getId(){ + function asVar() { + return $this->getName(); + } + + function getId() { return $this->id; } - function getName(){ + function getName() { return $this->ht['name']; } - function getEmailId(){ + function getEmailId() { return $this->ht['email_id']; } - function getEmail(){ + function getEmail() { if(!$this->email && $this->getEmailId()) $this->email=Email::lookup($this->getEmailId()); @@ -80,16 +84,16 @@ class Dept { return $this->email; } - function getNumStaff(){ + function getNumStaff() { return $this->ht['users']; } - function getNumUsers(){ + function getNumUsers() { return $this->getNumStaff(); } - function getNumMembers(){ + function getNumMembers() { return count($this->getMembers()); } @@ -117,11 +121,11 @@ class Dept { } - function getSLAId(){ + function getSLAId() { return $this->ht['sla_id']; } - function getSLA(){ + function getSLA() { if(!$this->sla && $this->getSLAId()) $this->sla=SLA::lookup($this->getSLAId()); @@ -164,11 +168,11 @@ class Dept { return ($this->getSignature() && $this->isPublic()); } - function getManagerId(){ + function getManagerId() { return $this->ht['manager_id']; } - function getManager(){ + function getManager() { if(!$this->manager && $this->getManagerId()) $this->manager=Staff::lookup($this->getManagerId()); @@ -176,19 +180,27 @@ class Dept { return $this->manager; } - function isPublic(){ + function isManager($staff) { + + if(is_object($staff)) $staff=$staff->getId(); + + return ($this->getManagerId() && $this->getManagerId()==$staff); + } + + + function isPublic() { return ($this->ht['ispublic']); } - function autoRespONNewTicket(){ + function autoRespONNewTicket() { return ($this->ht['ticket_auto_response']); } - function autoRespONNewMessage(){ + function autoRespONNewMessage() { return ($this->ht['message_auto_response']); } - function noreplyAutoResp(){ + function noreplyAutoResp() { return ($this->ht['noreply_autoresp']); } @@ -201,7 +213,7 @@ class Dept { return $this->ht; } - function getInfo(){ + function getInfo() { return $this->getHashtable(); } @@ -243,9 +255,9 @@ class Dept { } - function update($vars,&$errors){ + function update($vars, &$errors) { - if(!$this->save($this->getId(),$vars,$errors)) + if(!$this->save($this->getId(), $vars, $errors)) return false; $this->updateAllowedGroups($vars['groups']); @@ -262,7 +274,7 @@ class Dept { $id=$this->getId(); $sql='DELETE FROM '.DEPT_TABLE.' WHERE dept_id='.db_input($id).' LIMIT 1'; - if(db_query($sql) && ($num=db_affected_rows())){ + 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)); @@ -287,7 +299,7 @@ class Dept { return $id; } - function lookup($id){ + function lookup($id) { return ($id && is_numeric($id) && ($dept = new Dept($id)) && $dept->getId()==$id)?$dept:null; } @@ -304,12 +316,15 @@ class Dept { return ($cfg && $cfg->getDefaultDeptId() && ($name=Dept::getNameById($cfg->getDefaultDeptId())))?$name:null; } - function getDepartments( $publiconly=false) { + function getDepartments( $criteria=null) { $depts=array(); - $sql ='SELECT dept_id, dept_name FROM '.DEPT_TABLE; - if($publiconly) - $sql.=' WHERE ispublic=1'; + $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' WHERE 1'; + if($criteria['publiconly']) + $sql.=' AND ispublic=1'; + + if(($manager=$criteria['manager'])) + $sql.=' AND manager_id='.db_input(is_object($manager)?$manager->getId():$manager); if(($res=db_query($sql)) && db_num_rows($res)) { while(list($id, $name)=db_fetch_row($res)) @@ -320,17 +335,17 @@ class Dept { } function getPublicDepartments() { - return self::getDepartments(true); + return self::getDepartments(array('publiconly'=>true)); } - function create($vars,&$errors) { + function create($vars, &$errors) { if(($id=self::save(0, $vars, $errors)) && ($dept=self::lookup($id))) $dept->updateAllowedGroups($vars['groups']); return $id; } - function save($id,$vars,&$errors) { + function save($id, $vars, &$errors) { global $cfg; if($id && $id!=$vars['id']) @@ -342,11 +357,11 @@ class Dept { if(!is_numeric($vars['tpl_id'])) $errors['tpl_id']='Template selection required'; - if(!$vars['name']){ + if(!$vars['name']) { $errors['name']='Name required'; - }elseif(strlen($vars['name'])<4) { + } elseif(strlen($vars['name'])<4) { $errors['name']='Name is too short.'; - }elseif(($did=Dept::getIdByName($vars['name'])) && $did!=$id){ + } elseif(($did=Dept::getIdByName($vars['name'])) && $did!=$id) { $errors['name']='Department already exist'; } @@ -377,7 +392,7 @@ class Dept { $errors['err']='Unable to update '.Format::htmlchars($vars['name']).' Dept. Error occurred'; - }else{ + } else { $sql='INSERT INTO '.DEPT_TABLE.' '.$sql.',created=NOW()'; if(db_query($sql) && ($id=db_insert_id())) return $id; diff --git a/include/class.file.php b/include/class.file.php index e4012bb4b9f7665438cc28fbc3818162f48579a7..364e7f14ac570d3cc0c2bb2fa074bb05eef1c29a 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -27,7 +27,8 @@ class AttachmentFile { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT f.*, count(DISTINCT c.canned_id) as canned, count(DISTINCT t.ticket_id) as tickets ' + $sql='SELECT id, type, size, name, hash, f.created, ' + .' 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) ' @@ -90,18 +91,36 @@ class AttachmentFile { return $this->ht['hash']; } - function getBinary() { - return $this->ht['filedata']; + function open() { + return new AttachmentChunkedData($this->id); + } + + function sendData() { + $file = $this->open(); + while ($chunk = $file->read()) + echo $chunk; } function getData() { - return $this->getBinary(); + # XXX: This is horrible, and is subject to php's memory + # restrictions, etc. Don't use this function! + ob_start(); + $this->sendData(); + $data = &ob_get_contents(); + ob_end_clean(); + return $data; } function delete() { $sql='DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1'; - return (db_query($sql) && db_affected_rows()); + if(!db_query($sql) || !db_affected_rows()) + return false; + + //Delete file data. + AttachmentChunkedData::deleteOrphans(); + + return true; } @@ -110,7 +129,7 @@ class AttachmentFile { header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream')); header('Content-Length: '.$this->getSize()); - echo $this->getData(); + $this->sendData(); exit(); } @@ -132,7 +151,7 @@ class AttachmentFile { header('Content-Transfer-Encoding: binary'); header('Content-Length: '.$this->getSize()); - echo $this->getBinary(); + $this->sendData(); exit(); } @@ -168,15 +187,9 @@ class AttachmentFile { if (!(db_query($sql) && ($id=db_insert_id()))) return false; - foreach (str_split($file['data'], 1024*100) as $chunk) { - $sql='UPDATE '.FILE_TABLE - .' SET filedata = CONCAT(filedata,'.db_input($chunk).')' - .' WHERE id='.db_input($id); - if(!db_query($sql)) { - db_query('DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($id).' LIMIT 1'); - return false; - } - } + $data = new AttachmentChunkedData($id); + if (!$data->write($file['data'])) + return false; return $id; } @@ -202,8 +215,8 @@ class AttachmentFile { * canned-response, or faq point to any more. */ /* static */ function deleteOrphans() { - $res=db_query( - 'DELETE FROM '.FILE_TABLE.' WHERE id NOT IN (' + + $sql = 'DELETE FROM '.FILE_TABLE.' WHERE id NOT IN (' # DISTINCT implies sort and may not be necessary .'SELECT DISTINCT(file_id) FROM (' .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE @@ -212,45 +225,67 @@ class AttachmentFile { .' UNION ALL ' .'SELECT file_id FROM '.FAQ_ATTACHMENT_TABLE .') still_loved' - .')'); - return db_affected_rows(); + .')'; + + db_query($sql); + + //Delete orphaned chuncked data! + AttachmentChunkedData::deleteOrphans(); + + return true; + } } -class AttachmentList { - function AttachmentList($table, $key) { - $this->table = $table; - $this->key = $key; +/** + * Attachments stored in the database are cut into 256kB chunks and stored + * in the FILE_CHUNK_TABLE to overcome the max_allowed_packet limitation of + * LOB fields in the MySQL database + */ +define('CHUNK_SIZE', 500*1024); # Beware if you change this... +class AttachmentChunkedData { + function AttachmentChunkedData($file) { + $this->_file = $file; + $this->_pos = 0; } - 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 length() { + list($length) = db_fetch_row(db_query( + 'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE + .' WHERE file_id='.db_input($this->_file))); + return $length; } - - function getCount() { - return count($this->all()); + + function read() { + # Read requested length of data from attachment chunks + list($buffer) = @db_fetch_row(db_query( + 'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id=' + .db_input($this->_file).' AND chunk_id='.$this->_pos++)); + return $buffer; } - function add($fileId) { - db_query( - 'INSERT INTO '.$this->table - .' SET '.$this->key - .' file_id='.db_input($fileId)); + function write($what, $chunk_size=CHUNK_SIZE) { + $offset=0; + for (;;) { + $block = substr($what, $offset, $chunk_size); + if (!$block) break; + if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE + .' SET filedata=0x'.bin2hex($block).', file_id=' + .db_input($this->_file).', chunk_id='.db_input($this->_pos++))) + return false; + $offset += strlen($block); + } + + return $this->_pos; } - function remove($fileId) { - db_query( - 'DELETE FROM '.$this->table - .' WHERE '.$this->key - .' AND file_id='.db_input($fileId)); + function deleteOrphans() { + + $sql = 'DELETE c.* FROM '.FILE_CHUNK_TABLE.' c ' + . ' LEFT JOIN '.FILE_TABLE.' f ON(f.id=c.file_id) ' + . ' WHERE f.id IS NULL'; + + return db_query($sql)?db_affected_rows():0; } } ?> diff --git a/include/class.filter.php b/include/class.filter.php index b6c81b42b60d0d4b8c67d3f7b33b1138695fa1e7..9c172edaabaf3f819df46e49e5fd2da33eb90d62 100644 --- a/include/class.filter.php +++ b/include/class.filter.php @@ -2,7 +2,7 @@ /********************************************************************* class.filter.php - Email Filter Class + Ticket Filter Peter Rotich <peter@osticket.com> Copyright (c) 2006-2012 osTicket @@ -18,7 +18,7 @@ class Filter { var $id; var $ht; - function Filter($id){ + function Filter($id) { $this->id=0; $this->load($id); } @@ -29,8 +29,8 @@ class Filter { 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) ' + .' FROM '.FILTER_TABLE.' filter ' + .' LEFT JOIN '.FILTER_RULE_TABLE.' rule ON(rule.filter_id=filter.id) ' .' WHERE filter.id='.db_input($id) .' GROUP BY filter.id'; @@ -47,27 +47,31 @@ class Filter { return $this->load($this->getId()); } - function getId(){ + function getId() { return $this->id; } - function getName(){ + function getTarget() { + return $this->ht['target']; + } + + function getName() { return $this->ht['name']; } - function getNotes(){ + function getNotes() { return $this->ht['notes']; } - function getInfo(){ + function getInfo() { return $this->ht; } - function getNumRules(){ + function getNumRules() { return $this->ht['rule_count']; } - function getExecOrder(){ + function getExecOrder() { return $this->ht['execorder']; } @@ -75,7 +79,7 @@ class Filter { return $this->ht['email_id']; } - function isActive(){ + function isActive() { return ($this->ht['isactive']); } @@ -83,23 +87,23 @@ class Filter { return !strcasecmp($this->getName(),'SYSTEM BAN LIST'); } - function getDeptId(){ + function getDeptId() { return $this->ht['dept_id']; } - function getPriorityId(){ + function getPriorityId() { return $this->ht['priority_id']; } - function getSLAId(){ + function getSLAId() { return $this->ht['sla_id']; } - function getStaffId(){ + function getStaffId() { return $this->ht['staff_id']; } - function getTeamId(){ + function getTeamId() { return $this->ht['team_id']; } @@ -107,36 +111,36 @@ class Filter { return $this->ht['canned_response_id']; } - function stopOnMatch(){ + function stopOnMatch() { return ($this->ht['stop_on_match']); } - function matchAllRules(){ + function matchAllRules() { return ($this->ht['match_all_rules']); } - function rejectEmail(){ - return ($this->ht['reject_email']); + function rejectOnMatch() { + return ($this->ht['reject_ticket']); } - function useReplyToEmail(){ + function useReplyToEmail() { return ($this->ht['use_replyto_email']); } - function disableAlerts(){ + function disableAlerts() { return ($this->ht['disable_autoresponder']); } - function sendAlerts(){ + function sendAlerts() { return (!$this->disableAlerts()); } - function getRules(){ + 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)){ + $sql='SELECT * FROM '.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']); } @@ -145,11 +149,11 @@ class Filter { return $this->ht['rules']; } - function getFlatRules(){ //Format used on html... I'm ashamed + function getFlatRules() { //Format used on html... I'm ashamed $info=array(); - if(($rules=$this->getRules())){ - foreach($rules as $k=>$rule){ + if(($rules=$this->getRules())) { + foreach($rules as $k=>$rule) { $i=$k+1; $info["rule_w$i"]=$rule['w']; $info["rule_h$i"]=$rule['h']; @@ -169,7 +173,7 @@ class Filter { function removeRule($what, $how, $val) { - $sql='DELETE FROM '.EMAIL_FILTER_RULE_TABLE + $sql='DELETE FROM '.FILTER_RULE_TABLE .' WHERE filter_id='.db_input($this->getId()) .' AND what='.db_input($what) .' AND how='.db_input($how) @@ -187,17 +191,19 @@ class Filter { } function containsRule($what, $how, $val) { + $val = trim($val); if (isset($this->ht['rules'])) { + $match = array("w"=>$what, "h"=>$how, "v"=>$val); foreach ($this->ht['rules'] as $rule) { - if (array("w"=>$what, "h"=>$how, "v"=>$val) == $rule) { - return True; - } + if ($match == $rule) + return true; } - return False; + return false; + } else { # Fetch from database return 0 != db_count( - "SELECT COUNT(*) FROM ".EMAIL_FILTER_RULE_TABLE + "SELECT COUNT(*) FROM ".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) @@ -208,35 +214,33 @@ class Filter { * 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) + * $info is an ARRAY, which has valid keys + * email - FROM email address of the ticket owner + * name - name of ticket owner + * subject - subject line of the ticket + * body - body content of the message (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 + * emailId - osTicket system email id */ - function matches($email) { - $what = array( - "email" => $email['email'], - "subject" => $email['subject'], - # XXX: Support reply-to too ? - "name" => $email['name'], - "body" => $email['message'] - # XXX: Support headers - ); + function matches($what) { + + if(!$what || !is_array($what)) return false; + $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) + 'equal' => array('strcmp', 0), + 'not_equal' => array('strcmp', null, 0), + 'contains' => array('strpos', null, false), + 'dn_contain'=> array('strpos', false) ); + $match = false; # Respect configured filter email-id - if ($this->getEmailId() && $this->getEmailId() != $email['emailId']) + if ($this->getEmailId() + && !strcasecmp($this->getTarget(), 'Email') + && $this->getEmailId() != $what['emailId']) return false; foreach ($this->getRules() as $rule) { @@ -259,13 +263,14 @@ class Filter { } } } + 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) { + function apply(&$ticket, $info=null) { # TODO: Disable alerting # XXX: Does this imply turning it on as well? (via ->sendAlerts()) if ($this->disableAlerts()) $ticket['autorespond']=false; @@ -279,22 +284,24 @@ class Filter { # 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 + # Override name with reply-to information from the TicketFilter # match - if ($this->useReplyToEmail() && $email['reply-to']) { - $ticket['email'] = $email['reply-to']; - if ($email['reply-to-name']) - $ticket['name'] = $email['reply-to-name']; + if ($this->useReplyToEmail() && $info['reply-to']) { + $ticket['email'] = $info['reply-to']; + if ($info['reply-to-name']) + $ticket['name'] = $info['reply-to-name']; } + + # Use canned response. if ($this->getCannedResponse()) $ticket['cannedResponseId'] = $this->getCannedResponse(); } /* static */ function getSupportedMatches() { return array( - 'name'=> "Sender's Name", - 'email'=> "Sender's Email", - 'subject'=> 'Email Subject', - 'body'=> 'Email Body/Text' + 'name'=> 'Name', + 'email'=> 'Email', + 'subject'=> 'Subject', + 'body'=> 'Body/Text' ); } /* static */ function getSupportedMatchTypes() { @@ -306,7 +313,7 @@ class Filter { ); } - function update($vars,&$errors){ + function update($vars,&$errors) { if(!Filter::save($this->getId(),$vars,$errors)) return false; @@ -316,47 +323,55 @@ class Filter { return true; } - function delete(){ + 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)); + $sql='DELETE FROM '.FILTER_TABLE.' WHERE id='.db_input($id).' LIMIT 1'; + if(db_query($sql) && ($num=db_affected_rows())) { + db_query('DELETE FROM '.FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id)); } return $num; } /** static functions **/ - function create($vars,&$errors){ + function getTargets() { + return array( + 'Any' => 'Any', + 'Web' => 'Web Forms', + 'API' => 'API Calls', + 'Email' => 'Emails'); + } + + function create($vars,&$errors) { return Filter::save(0,$vars,$errors); } - function getIdByName($name){ + function getIdByName($name) { - $sql='SELECT id FROM '.EMAIL_FILTER_TABLE.' WHERE name='.db_input($name); + $sql='SELECT id FROM '.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){ + function lookup($id) { return ($id && is_numeric($id) && ($f= new Filter($id)) && $f->getId()==$id)?$f:null; } - function validate_rules($vars,&$errors){ + function validate_rules($vars,&$errors) { return self::save_rules(0,$vars,$errors); } - function save_rules($id,$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"] || $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)) @@ -367,7 +382,7 @@ class Filter { $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"]){ + }elseif($vars["rule_v$i"]) { $errors["rule_$i"]='Incomplete selection'; } } @@ -383,7 +398,7 @@ class Filter { 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)); + db_query('DELETE FROM '.FILTER_RULE_TABLE.' WHERE filter_id='.db_input($id)); $num=0; foreach($rules as $rule) { $rule['filter_id']=$id; @@ -394,39 +409,52 @@ class Filter { return $num; } - function save($id,$vars,&$errors){ + function save($id,$vars,&$errors) { if(!$vars['execorder']) - $errors['execorder']='Order required'; + $errors['execorder'] = 'Order required'; elseif(!is_numeric($vars['execorder'])) - $errors['execorder']='Must be numeric value'; + $errors['execorder'] = 'Must be numeric value'; if(!$vars['name']) - $errors['name']='Name required'; + $errors['name'] = 'Name required'; elseif(($sid=self::getIdByName($vars['name'])) && $sid!=$id) - $errors['name']='Name already in-use'; + $errors['name'] = 'Name already in-use'; if(!$errors && !self::validate_rules($vars,$errors) && !$errors['rules']) - $errors['rules']='Unable to validate rules as entered'; + $errors['rules'] = 'Unable to validate rules as entered'; + + $targets = self::getTargets(); + if(!$vars['target']) + $errors['target'] = 'Target required'; + else if(!is_numeric($vars['target']) && !$targets[$vars['target']]) + $errors['target'] = 'Unknown or invalid target'; 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). - ',canned_response_id='.db_input($vars['canned_response_id']). - ',notes='.db_input($vars['notes']); + $emailId = 0; + if(is_numeric($vars['target'])) { + $emailId = $vars['target']; + $vars['target'] = 'Email'; + } + + $sql=' updated=NOW() ' + .',isactive='.db_input($vars['isactive']) + .',target='.db_input($vars['target']) + .',name='.db_input($vars['name']) + .',execorder='.db_input($vars['execorder']) + .',email_id='.db_input($emailId) + .',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_ticket='.db_input(isset($vars['reject_ticket'])?1:0) + .',use_replyto_email='.db_input(isset($vars['use_replyto_email'])?1:0) + .',disable_autoresponder='.db_input(isset($vars['disable_autoresponder'])?1:0) + .',canned_response_id='.db_input($vars['canned_response_id']) + .',notes='.db_input($vars['notes']); //Auto assign ID is overloaded... @@ -438,11 +466,11 @@ class Filter { $sql.=',staff_id=0,team_id=0 '; //no auto-assignment! if($id) { - $sql='UPDATE '.EMAIL_FILTER_TABLE.' SET '.$sql.' WHERE id='.db_input($id); + $sql='UPDATE '.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() '; + $sql='INSERT INTO '.FILTER_TABLE.' SET '.$sql.',created=NOW() '; if(!db_query($sql) || !($id=db_insert_id())) $errors['err']='Unable to add filter. Internal error'; } @@ -464,14 +492,14 @@ class FilterRule { var $filter; - function FilterRule($id,$filterId=0){ + 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 ' + $sql='SELECT rule.* FROM '.FILTER_RULE_TABLE.' rule ' .' WHERE rule.id='.db_input($id); if($filterId) $sql.=' AND rule.filter_id='.db_input($filterId); @@ -529,9 +557,9 @@ class FilterRule { return true; } - function delete(){ + function delete() { - $sql='DELETE FROM '.EMAIL_FILTER_RULE_TABLE.' WHERE id='.db_input($this->getId()).' AND filter_id='.db_input($this->getFilterId()); + $sql='DELETE FROM '.FILTER_RULE_TABLE.' WHERE id='.db_input($this->getId()).' AND filter_id='.db_input($this->getFilterId()); return (db_query($sql) && db_affected_rows()); } @@ -559,12 +587,12 @@ class FilterRule { $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']); + $sql='UPDATE '.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; + $sql='INSERT INTO '.FILTER_RULE_TABLE.' SET created=NOW(), filter_id='.db_input($vars['filter_id']).', '.$sql; if(db_query($sql) && ($id=db_insert_id())) return $id; } @@ -579,68 +607,95 @@ class FilterRule { } /** - * Applies rules defined in the staff control panel "Email Filters". Each + * Applies rules defined in the admin control panel > Settings tab > "Ticket 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 + * the incoming tickets against the defined rules, and, if the email matches, + * the ticket will be modified as described in the filter actions. */ -class EmailFilter { +class TicketFilter { + + var $target; + var $vars; + /** - * 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). + * Construct a list of filters to handle a new ticket + * taking into account the source/origin of the ticket. * - * $email is an ARRAY, which has valid keys - * *email - email address of sender - * name - name of sender - * subject - subject line of the email - * email-id - id of osTicket email recipient address + * $vars is an ARRAY, which has valid keys + * *email - email address of user + * name - name of user + * subject - subject of the ticket + * emailId - id of osTicket's system email (for emailed tickets) * --------------- * @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. + * IF $vars is not provided, every (active) filter will be fetched from the + * database and matched against the incoming ticket. Otherwise, a subset + * of filters from the database that appear to have rules that + * deal with the data in the incoming ticket (based on $vars) 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['email'], $email['name'], - $email['subject'], $email['emailId'])); - } + function TicketFilter($origin, $vars=null) { + + //Normalize the target based on ticket's origin. + $this->target = self::origin2target($origin); + + //Extract the vars we care about (fields we filter by!). + $this->vars = array_filter(array_map('trim', + array( + 'email' => $vars['email'], + 'subject' => $vars['subject'], + 'name' => $vars['name'], + 'body' => $vars['message'], + 'emailId' => $vars['emailId']) + )); + + //Init filters. + $this->build(); } - - function build($res) { + + function build() { + + //Clear any memoized filters $this->filters = array(); - while (list($id) = db_fetch_row($res)) - array_push($this->filters, new Filter($id)); + $this->short_list = null; + + //Query DB for "possibly" matching filters. + $res = $this->vars?$this->quickList():$this->getAllActive(); + if($res) { + while (list($id) = db_fetch_row($res)) + array_push($this->filters, new Filter($id)); + } + return $this->filters; } + + function getTarget() { + return $this->target; + } + /** - * Fetches the short list of filters that match the email received in the + * Fetches the short list of filters that match the ticket vars received in the * constructor. This function is memoized so subsequent calls will * return immediately. */ function getMatchingFilterList() { + if (!isset($this->short_list)) { $this->short_list = array(); foreach ($this->filters as $filter) - if ($filter->matches($this->email)) + if ($filter->matches($this->vars)) $this->short_list[] = $filter; } + return $this->short_list; } /** - * Determine if the filters that match the received email indicate that + * Determine if the filters that match the received vars indicate that * the email should be rejected * * Returns FALSE if the email should be acceptable. If the email should - * be rejected, the first filter that matches and has rejectEmail set is + * be rejected, the first filter that matches and has reject ticket set is * returned. */ function shouldReject() { @@ -649,7 +704,7 @@ class EmailFilter { # be blocked; however, don't unset $reject, because if it # was set by another rule that did not set stopOnMatch(), we # should still honor its configuration - if ($filter->rejectEmail()) return $filter; + if ($filter->rejectOnMatch()) return $filter; } return false; } @@ -659,17 +714,26 @@ class EmailFilter { */ function apply(&$ticket) { foreach ($this->getMatchingFilterList() as $filter) { - $filter->apply($ticket, $this->email); + $filter->apply($ticket, $this->vars); if ($filter->stopOnMatch()) break; } } /* static */ function getAllActive() { - $sql="SELECT id FROM ".EMAIL_FILTER_TABLE." WHERE isactive" - ." ORDER BY execorder"; + + $sql='SELECT id FROM '.FILTER_TABLE + .' WHERE isactive=1 ' + .' AND target IN ("Any", '.db_input($this->getTarget()).') '; + + #Take into account email ID. + if($this->vars['emailId']) + $sql.=' AND (email_id=0 OR email_id='.db_input($this->vars['emailId']).')'; + + $sql.=' 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 @@ -693,48 +757,60 @@ class EmailFilter { * 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); + function quickList() { + + if(!$this->vars || !$this->vars['email']) + return $this->getAllActive(); + + $sql='SELECT DISTINCT filter_id FROM '.FILTER_RULE_TABLE.' rule ' + .' INNER JOIN '.FILTER_TABLE.' filter ' + .' ON (filter.id=rule.filter_id) ' + .' WHERE filter.isactive ' + ." AND filter.target IN ('Any', ".db_input($this->getTarget()).') '; + + # Filter by system's email-id if specified + if($this->vars['emailId']) + $sql.=' AND (filter.email_id=0 OR filter.email_id='.db_input($this->vars['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)."))"; + $sql.=" AND ((what='email' AND LOCATE(val, ".db_input($this->vars['email']).'))'; + if($this->vars['name']) + $sql.=" OR (what='name' AND LOCATE(val, ".db_input($this->vars['name']).'))'; + if($this->vars['subject']) + $sql.=" OR (what='subject' AND LOCATE(val, ".db_input($this->vars['subject']).'))'; + + # 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" + ." FROM ".FILTER_RULE_TABLE." rule" + ." INNER JOIN ".FILTER_TABLE." filter" ." ON (rule.filter_id=filter.id)" + ." WHERE filter.isactive" + ." AND filter.target IN('Any', ".db_input($this->getTarget()).")" ." 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"; + if (!$this->vars['name']) $sql.=" AND COUNT(*)-COUNT(NULLIF(what,'name'))=0"; + if (!$this->vars['subject']) $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 + # have at least one rule 'what' type that wasn't considered e.g body $sql.=") OR filter.id IN (" ." SELECT filter_id" - ." FROM ".EMAIL_FILTER_RULE_TABLE." rule" - ." INNER JOIN ".EMAIL_FILTER_TABLE." filter" + ." FROM ".FILTER_RULE_TABLE." rule" + ." INNER JOIN ".FILTER_TABLE." filter" ." ON (rule.filter_id=filter.id)" - ." WHERE what NOT IN ('email'" + ." WHERE filter.isactive" + ." AND filter.target IN ('Any', ".db_input($this->getTarget()).")" + ." AND what NOT IN ('email'" # Handle sender-name and subject if specified - .(($name!==false)?",'name'":"") - .(($subj!==false)?",'subject'":"") - .") AND filter.match_all_rules = false" + .((!$this->vars['name'])?",'name'":"") + .((!$this->vars['subject'])?",'subject'":"") + .") AND filter.match_all_rules = 0 " # Return filters in declared execution order .") ORDER BY filter.execorder"; - + return db_query($sql); } /** @@ -751,10 +827,10 @@ class EmailFilter { /* static */ function isBanned($addr) { $sql='SELECT filter.id, what, how, UPPER(val) ' - .' FROM '.EMAIL_FILTER_TABLE.' filter' - .' INNER JOIN '.EMAIL_FILTER_RULE_TABLE.' rule' + .' FROM '.FILTER_TABLE.' filter' + .' INNER JOIN '.FILTER_RULE_TABLE.' rule' .' ON (filter.id=rule.filter_id)' - .' WHERE filter.reject_email' + .' WHERE filter.reject_ticket' .' AND filter.match_all_rules=0' .' AND filter.email_id=0' .' AND filter.isactive' @@ -762,20 +838,22 @@ class EmailFilter { .' AND rule.what="email"' .' AND LOCATE(rule.val,'.db_input($addr).')'; + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + # 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']; - } + + while ($row=db_fetch_array($res)) { + list($func, $pos, $neg) = $how[$row['how']]; + if (!$func) continue; + $result = call_user_func($func, $addr, $row['val']); + if (($neg === null && $result === $pos) || $result !== $neg) + return $row['id']; } + return false; } @@ -814,5 +892,15 @@ class EmailFilter { } return false; } + + /** + * Normalize ticket source to supported filter target + * + */ + function origin2target($origin) { + $sources=array('web' => 'Web', 'email' => 'Email', 'phone' => 'Web', 'staff' => 'Web', 'api' => 'API'); + + return $sources[strtolower($origin)]; + } } ?> diff --git a/include/class.format.php b/include/class.format.php index 9de1197edbb3d4272193f67af48826851f72296e..aaa6667d32c2fce14da2cb9f5e15387ec9608b0c 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -180,7 +180,7 @@ class Format { } /* elapsed time */ - function elapsedTime($sec){ + function elapsedTime($sec) { if(!$sec || !is_numeric($sec)) return ""; @@ -197,32 +197,32 @@ class Format { /* 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)); + return Format::userdate($cfg->getDateFormat(), Misc::db2gmtime($time)); } function db_datetime($time) { global $cfg; - return Format::userdate($cfg->getDateTimeFormat(),Misc::db2gmtime($time)); + return Format::userdate($cfg->getDateTimeFormat(), Misc::db2gmtime($time)); } function db_daydatetime($time) { global $cfg; - return Format::userdate($cfg->getDayDateTimeFormat(),Misc::db2gmtime($time)); + return Format::userdate($cfg->getDayDateTimeFormat(), Misc::db2gmtime($time)); } - function userdate($format,$gmtime) { - return Format::date($format,$gmtime,$_SESSION['TZ_OFFSET'],$_SESSION['TZ_DST']); + function userdate($format, $gmtime) { + return Format::date($format, $gmtime, $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST']); } - 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))); - } - + 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.lock.php b/include/class.lock.php index caa1be71a99ba8b35a2406658aa974e11512cdea..6baa146891a1367e4cfa47742c9711e49a5451b5 100644 --- a/include/class.lock.php +++ b/include/class.lock.php @@ -88,7 +88,7 @@ class TicketLock { function renew($lockTime=0) { if(!$lockTime || !is_numeric($lockTime)) //XXX: test to make it works. - $lockTime = '(TIME_TO_SEC(TIMEDIFF(created,expire))/60)'; + $lockTime = '(TIME_TO_SEC(TIMEDIFF(expire,created))/60)'; $sql='UPDATE '.TICKET_LOCK_TABLE diff --git a/include/class.mailer.php b/include/class.mailer.php index a469d5528f6598489a8a8ecd63b795594f5db322..2a965eb371cf3d3cf952e48be313f533bdd084a5 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -65,8 +65,8 @@ class Mailer { function getFromAddress() { - if(!$this->ht['from'] && $this->getEmail()) - $this->ht['from'] =$this->getEmail()->getAddress(); + if(!$this->ht['from'] && ($email=$this->getEmail())) + $this->ht['from'] =sprintf('"%s" <%s>', ($email->getName()?$email->getName():$email->getEmail()), $email->getEmail()); return $this->ht['from']; } @@ -106,8 +106,7 @@ class Mailer { 'Subject' => $subject, 'Date'=> date('D, d M Y H:i:s O'), 'Message-ID' => $messageId, - 'X-Mailer' =>'osTicket Mailer', - 'Content-Type' => 'text/html; charset="UTF-8"' + 'X-Mailer' =>'osTicket Mailer' ); $mime = new Mail_mime(); @@ -134,7 +133,7 @@ class Mailer { //encode the body $body = $mime->get($encodings); //encode the headers. - $headers = $mime->headers($headers); + $headers = $mime->headers($headers, true); if(($smtp=$this->getSMTPInfo())) { //Send via SMTP $mail = mail::factory('smtp', array ('host' => $smtp['host'], @@ -142,7 +141,7 @@ class Mailer { 'auth' => $smtp['auth'], 'username' => $smtp['username'], 'password' => $smtp['password'], - 'timeout' =>20, + 'timeout' => 20, 'debug' => false, )); diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index fb8402169e83b39c258118329215d2459d7547a2..aa43ce89e960231bdf2e25bf0f622a24d2b44741 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -376,7 +376,7 @@ class MailFetcher { return true; //Reporting success so the email can be moved or deleted. //Is the email address banned? - if($mailinfo['email'] && EmailFilter::isBanned($mailinfo['email'])) { + if($mailinfo['email'] && TicketFilter::isBanned($mailinfo['email'])) { //We need to let admin know... $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['email']); return true; //Report success (moved or delete) @@ -442,7 +442,7 @@ class MailFetcher { //This should be really a comment on message - NoT an internal note. //TODO: support comments on Messages and Responses. $error = sprintf('Attachment %s [%s] rejected because of file type', $a['name'], $a['mime']); - $ticket->postNote('Email Attachment Rejected', $error, false); + $ticket->postNote('Email Attachment Rejected', $error, 'SYSTEM', false); $ost->logDebug('Email Attachment Rejected (Ticket #'.$ticket->getExtId().')', $error); } } diff --git a/include/class.misc.php b/include/class.misc.php index 9fd6744ba8913061a5e34c2b10bf705216d286fa..8149a1fce37ea3a26106f6bb217d1c92cc17395e 100644 --- a/include/class.misc.php +++ b/include/class.misc.php @@ -35,7 +35,7 @@ class Misc { if(!$var) return; $dbtime=is_int($var)?$var:strtotime($var); - return $dbtime-($cfg->getMysqlTZoffset()*3600); + return $dbtime-($cfg->getDBTZoffset()*3600); } //Take user time or gmtime and return db (mysql) time. @@ -50,7 +50,7 @@ class Misc { $time=$time-($offset*3600); } //gm to db time - return $time+($cfg->getMysqlTZoffset()*3600); + return $time+($cfg->getDBTZoffset()*3600); } /*Helper get GM time based on timezone offset*/ diff --git a/include/class.nav.php b/include/class.nav.php index 0d5fddcbf3a48772db052a71363bfe0fb3b64ad5..eb66293770888faa572cce786b2d2b22c868ed67 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -43,7 +43,7 @@ class StaffNav { return (!$this->isAdminPanel()); } - function setTabActive($tab){ + function setTabActive($tab, $menu=''){ if($this->tabs[$tab]){ $this->tabs[$tab]['active']=true; @@ -51,6 +51,7 @@ class StaffNav { $this->tabs[$this->activetab]['active']=false; $this->activetab=$tab; + if($menu) $this->setActiveSubMenu($menu, $tab); return true; } @@ -58,16 +59,25 @@ class StaffNav { return false; } - function setActiveTab($tab){ - return $this->setTabActive($tab); + function setActiveTab($tab, $menu=''){ + return $this->setTabActive($tab, $menu); } function getActiveTab(){ return $this->activetab; } - function setActiveSubMenu($mid) { - $this->activeMenu = $mid; + function setActiveSubMenu($mid, $tab='') { + if(is_numeric($mid)) + $this->activeMenu = $mid; + elseif($mid && $tab && ($subNav=$this->getSubNav($tab))) { + foreach($subNav as $k => $menu) { + if(strcasecmp($mid, $menu['href'])) continue; + + $this->activeMenu = $k+1; + break; + } + } } function getActiveMenu() { @@ -162,12 +172,11 @@ class AdminNav extends StaffNav{ if(!$this->tabs){ $tabs=array(); - $tabs['dashboard']=array('desc'=>'Dashboard','href'=>'admin.php','title'=>'Admin Dashboard'); + $tabs['dashboard']=array('desc'=>'Dashboard','href'=>'logs.php','title'=>'Admin Dashboard'); $tabs['settings']=array('desc'=>'Settings','href'=>'settings.php','title'=>'System Settings'); + $tabs['manage']=array('desc'=>'Manage','href'=>'helptopics.php','title'=>'Manage Options'); $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'); + $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Manage Staff'); $this->tabs=$tabs; } @@ -184,37 +193,32 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>'System Logs','href'=>'logs.php','iconclass'=>'logs'); break; case 'settings': - $subnav[]=array('desc'=>'Settings & Preferences','href'=>'settings.php','iconclass'=>'preferences'); + $subnav[]=array('desc'=>'System Preferences','href'=>'settings.php?t=system','iconclass'=>'preferences'); + $subnav[]=array('desc'=>'Tickets','href'=>'settings.php?t=tickets','iconclass'=>'ticket-settings'); + $subnav[]=array('desc'=>'Emails','href'=>'settings.php?t=emails','iconclass'=>'email-settings'); + $subnav[]=array('desc'=>'Knowledgebase','href'=>'settings.php?t=kb','iconclass'=>'kb-settings'); + $subnav[]=array('desc'=>'Autoresponder','href'=>'settings.php?t=autoresp','iconclass'=>'email-autoresponders'); + $subnav[]=array('desc'=>'Alerts & Notices','href'=>'settings.php?t=alerts','iconclass'=>'alert-settings'); + break; + case 'manage': + $subnav[]=array('desc'=>'Help Topics','href'=>'helptopics.php','iconclass'=>'helpTopics'); + $subnav[]=array('desc'=>'Ticket Filters','href'=>'filters.php', + 'title'=>'Ticket Filters','iconclass'=>'ticketFilters'); $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', + $subnav[]=array('desc'=>'Emails','href'=>'emails.php', 'title'=>'Email Addresses', 'iconclass'=>'emailSettings'); + $subnav[]=array('desc'=>'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); + $subnav[]=array('desc'=>'Templates','href'=>'templates.php','title'=>'Email Templates','iconclass'=>'emailTemplates'); + $subnav[]=array('desc'=>'Diagnostic','href'=>'emailtest.php', 'title'=>'Email Diagnostic', 'iconclass'=>'emailDiagnostic'); 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) diff --git a/include/class.osticket.php b/include/class.osticket.php index 033ec0f82accf22a97b855b39c12c4166fe580af..86d206f212d6450db9755ac4f5c77926ff2a53c8 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -30,6 +30,7 @@ class osTicket { var $warning; var $message; + var $title; //Custom title. html > head > title. var $headers; var $config; @@ -149,6 +150,17 @@ class osTicket { return (!$errors); } + /* Replace Template Variables */ + function replaceTemplateVariables($input, $vars=array()) { + + $replacer = new VariableReplacer(); + $replacer->assign(array_merge($vars, + array('url' => $this->getConfig()->getBaseUrl()) + )); + + return $replacer->replaceVars($input); + } + function addExtraHeader($header) { $this->headers[md5($header)] = $header; } @@ -157,6 +169,14 @@ class osTicket { return $this->headers; } + function setPageTitle($title) { + $this->title = $title; + } + + function getPageTitle() { + return $this->title; + } + function getErrors() { return $this->errors; } diff --git a/include/class.pdf.php b/include/class.pdf.php index 2257dcc17aa6641fabeeac32a3fcecc28bd7b2c0..87fcd69ad4a97f694bf60235c8c15f8ef5529d88 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -57,11 +57,8 @@ class Ticket2PDF extends FPDF $this->SetFont('Times', 'B', 16); $this->Image(FPDF_DIR . 'print-logo.png', null, 10, 0, 20); $this->SetX(200, 15); - $this->Cell(0, 15, "Support Ticket System", 0, 1, 'R', 0); + $this->Cell(0, 15, $cfg->getTitle(), 0, 1, 'R', 0); //$this->SetY(40); - $this->SetXY(60, 25); - $this->SetFont('Arial', 'B', 16); - $this->Cell(0, 3, 'Ticket #'.$this->getTicket()->getExtId(), 0, 2, 'L'); $this->SetX($this->lMargin); $this->Cell(0, 3, '', "B", 2, 'L'); $this->SetFont('Arial', 'I',10); @@ -77,17 +74,29 @@ class Ticket2PDF extends FPDF $this->SetY(-15); $this->Cell(0, 2, '', "T", 2, 'L'); $this->SetFont('Arial', 'I', 9); - $this->Cell(0, 7, 'Ticket printed by '.$thisstaff->getUserName().' on '.date('r'), 0, 0, 'L'); + $this->Cell(0, 7, 'Ticket #'.$this->getTicket()->getNumber().' printed by '.$thisstaff->getUserName().' on '.date('r'), 0, 0, 'L'); //$this->Cell(0,10,'Page '.($this->PageNo()-$this->pageOffset).' of {nb} '.$this->pageOffset.' '.$this->PageNo(),0,0,'R'); $this->Cell(0, 7, 'Page ' . ($this->PageNo() - $this->pageOffset), 0, 0, 'R'); } + function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') { + parent::Cell($w, $h, $this->_utf8($txt), $border, $ln, $align, $fill, $link); + } + function WriteText($w, $text, $border) { $this->SetFont('Times','',11); $this->MultiCell($w, 5, $text, $border, 'L'); } + + function _utf8($text) { + + if(function_exists('iconv')) + return iconv('UTF-8', 'windows-1252', $text); + + return utf8_encode($text); + } function _print() { @@ -95,8 +104,18 @@ class Ticket2PDF extends FPDF return; $w =(($this->w/2)-$this->lMargin); - $l = 40; + $l = 35; $c = $w-$l; + + + $this->SetFont('Arial', 'B', 11); + $this->cMargin = 0; + $this->SetFont('Arial', 'B', 11); + $this->SetTextColor(10, 86, 142); + $this->Cell($w, 7,'Ticket #'.$ticket->getNumber(), 0, 0, 'L'); + $this->Ln(7); + $this->cMargin = 3; + $this->SetTextColor(0); $this->SetDrawColor(220, 220, 220); $this->SetFillColor(244, 250, 255); $this->SetX($this->lMargin); @@ -131,14 +150,17 @@ class Ticket2PDF extends FPDF $this->SetFont('Arial', 'B', 11); $this->Cell($l, 7, 'Source', 1, 0, 'L', true); $this->SetFont(''); - $this->Cell($c, 7, ucfirst($ticket->getSource()), 1, 0, 'L', true); + $source = ucfirst($ticket->getSource()); + if($ticket->getIP()) + $source.=' ('.$ticket->getIP().')'; + $this->Cell($c, 7, $source, 1, 0, 'L', true); $this->Ln(15); $this->SetFont('Arial', 'B', 11); if($ticket->isOpen()) { $this->Cell($l, 7, 'Assigned To', 1, 0, 'L', true); $this->SetFont(''); - $this->Cell($c, 7, $ticket->isAssigned()?implode('/', $ticket->getAssignees()):' -- ', 1, 0, 'L', true); + $this->Cell($c, 7, $ticket->isAssigned()?$ticket->getAssigned():' -- ', 1, 0, 'L', true); } else { $closedby = 'unknown'; @@ -151,17 +173,18 @@ class Ticket2PDF extends FPDF } $this->SetFont('Arial', 'B', 11); - $this->Cell($l, 7, 'Subject', 1, 0, 'L', true); + $this->Cell($l, 7, 'Help Topic', 1, 0, 'L', true); $this->SetFont(''); - $this->Cell($c, 7, $ticket->getSubject(), 1, 1, 'L', true); + $this->Cell($c, 7, $ticket->getHelpTopic(), 1, 1, 'L', true); $this->SetFont('Arial', 'B', 11); - $this->Cell($l, 7, 'Last Response', 1, 0, 'L', true); + $this->Cell($l, 7, 'SLA Plan', 1, 0, 'L', true); $this->SetFont(''); - $this->Cell($c, 7, Format::db_datetime($ticket->getLastRespDate()), 1, 0, 'L', true); + $sla = $ticket->getSLA(); + $this->Cell($c, 7, $sla?$sla->getName():' -- ', 1, 0, 'L', true); $this->SetFont('Arial', 'B', 11); - $this->Cell($l, 7, 'Help Topic', 1, 0, 'L', true); + $this->Cell($l, 7, 'Last Response', 1, 0, 'L', true); $this->SetFont(''); - $this->Cell($c, 7, $ticket->getHelpTopic(), 1, 1, 'L', true); + $this->Cell($c, 7, Format::db_datetime($ticket->getLastRespDate()), 1, 1, 'L', true); $this->SetFont('Arial', 'B', 11); if($ticket->isOpen()) { $this->Cell($l, 7, 'Due Date', 1, 0, 'L', true); @@ -177,7 +200,15 @@ class Ticket2PDF extends FPDF $this->Cell($l, 7, 'Last Message', 1, 0, 'L', true); $this->SetFont(''); $this->Cell($c, 7, Format::db_datetime($ticket->getLastMsgDate()), 1, 1, 'L', true); - $this->Ln(10); + $this->Ln(5); + + $this->SetFont('Arial', 'B', 11); + $this->cMargin = 0; + $this->SetTextColor(10, 86, 142); + $this->Cell($w, 7,trim($ticket->getSubject()), 0, 0, 'L'); + $this->Ln(7); + $this->SetTextColor(0); + $this->cMargin = 3; //Table header colors (RGB) $colors = array('M'=>array(195, 217, 255), diff --git a/include/class.staff.php b/include/class.staff.php index 83c9ce2e58da2905c4716a28ff159dfb870074b1..9c6078c7f148e3a01612c05cac9c796422871269 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -73,6 +73,10 @@ class Staff { return $this->load(); } + function asVar() { + return $this->getName(); + } + function getHastable() { return $this->ht; } @@ -210,6 +214,13 @@ class Staff { function getDepts() { return $this->getDepartments(); } + + function getManagedDepartments() { + + return ($depts=Dept::getDepartments( + array('manager' => $this->getId()) + ))?array_keys($depts):array(); + } function getGroupId() { return $this->ht['group_id']; @@ -503,7 +514,6 @@ class Staff { } /**** Static functions ********/ - function getStaffMembers($availableonly=false) { $sql='SELECT s.staff_id,CONCAT_WS(", ",s.lastname, s.firstname) as name ' @@ -555,15 +565,21 @@ class Staff { if($_SESSION['_staff']['laststrike']) { if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) { - $errors['err']='You\'ve reached maximum failed login attempts allowed.'; + $errors['err']='Max. failed login attempts reached'; + $_SESSION['_staff']['laststrike'] = time(); //reset timer. } else { //Timeout is over. //Reset the counter for next round of attempts after the timeout. $_SESSION['_staff']['laststrike']=null; $_SESSION['_staff']['strikes']=0; } } + + if(!$username || !$passwd) + $errors['err'] = 'Username and password required'; + + if($errors) return false; - if(!$errors && ($user=new StaffSession($username)) && $user->getId() && $user->check_passwd($passwd)) { + if(($user=new StaffSession(trim($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()) @@ -571,21 +587,22 @@ class Staff { $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; + $_SESSION['_staff'] = array(); //clear. + $_SESSION['_staff']['userID'] = $username; $user->refreshSession(); //set the hash. - $_SESSION['TZ_OFFSET']=$user->getTZoffset(); - $_SESSION['TZ_DST']=$user->observeDaylight(); + $_SESSION['TZ_OFFSET'] = $user->getTZoffset(); + $_SESSION['TZ_DST'] = $user->observeDaylight(); + //Log debug info. $ost->logDebug('Staff login', sprintf("%s logged in [%s]", $user->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug. - $sid=session_id(); //Current ID + + //Regenerate session id. + $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=$ost->getSession()) && is_object($session) && $sid) + if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id()) $session->destroy($sid); - - session_write_close(); return $user; } @@ -596,14 +613,14 @@ class Staff { $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". + 'Username: '.$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"; - $ost->logWarning('Excessive login attempts ('.$_POST['username'].')', $alert, ($cfg->alertONLoginError())); + $ost->logWarning('Excessive login attempts ('.$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']. + $alert='Username: '.$username."\n".'IP: '.$_SERVER['REMOTE_ADDR']. "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_staff']['strikes']; - $ost->logWarning('Failed staff login attempt ('.$_POST['username'].')', $alert, false); + $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false); } return false; diff --git a/include/class.team.php b/include/class.team.php index cdf4cffcd3148fab082b2dd69f24733481aaeca0..367815b9637d7ab24d9e295379d11a1f6601df74 100644 --- a/include/class.team.php +++ b/include/class.team.php @@ -21,7 +21,7 @@ class Team { var $members; - function Team($id){ + function Team($id) { return $this->load($id); } @@ -47,30 +47,34 @@ class Team { return $this->id; } - function reload(){ + function reload() { return $this->load($this->getId()); } - function getId(){ + function asVar() { + return $this->getName(); + } + + function getId() { return $this->id; } - function getName(){ + function getName() { return $this->ht['name']; } - function getNumMembers(){ + function getNumMembers() { return $this->ht['members']; } - function getMembers(){ + function getMembers() { - if(!$this->members && $this->getNumMembers()){ + 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)){ + if(($res=db_query($sql)) && db_num_rows($res)) { while(list($id)=db_fetch_row($res)) if(($staff= Staff::lookup($id))) $this->members[]= $staff; @@ -87,18 +91,18 @@ class Team { .' AND staff_id='.db_input($staff->getId())) !== 0; } - function getLeadId(){ + function getLeadId() { return $this->ht['lead_id']; } - function getTeamLead(){ + function getTeamLead() { if(!$this->lead && $this->getLeadId()) $this->lead=Staff::lookup($this->getLeadId()); return $this->lead; } - function getLead(){ + function getLead() { return $this->getTeamLead(); } @@ -106,27 +110,27 @@ class Team { return $this->ht; } - function getInfo(){ + function getInfo() { return $this->getHashtable(); } - function isEnabled(){ + function isEnabled() { return ($this->ht['isenabled']); } - function isActive(){ + function isActive() { return $this->isEnabled(); } - function update($vars,&$errors) { + 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['remove'] && in_array($this->getLeadId(), $vars['remove'])) $vars['lead_id']=0; //Save the changes... - if(!Team::save($this->getId(),$vars,$errors)) + if(!Team::save($this->getId(), $vars, $errors)) return false; //Delete staff marked for removal... @@ -170,12 +174,12 @@ class Team { } /* ----------- Static function ------------------*/ - function lookup($id){ + function lookup($id) { return ($id && is_numeric($id) && ($team= new Team($id)) && $team->getId()==$id)?$team:null; } - function getIdbyName($name){ + function getIdbyName($name) { $sql='SELECT team_id FROM '.TEAM_TABLE.' WHERE name='.db_input($name); if(($res=db_query($sql)) && db_num_rows($res)) @@ -201,7 +205,7 @@ class Team { .' ORDER by t.name '; } if(($res=db_query($sql)) && db_num_rows($res)) { - while(list($id,$name)=db_fetch_row($res)) + while(list($id, $name)=db_fetch_row($res)) $teams[$id] = $name; } @@ -212,20 +216,20 @@ class Team { return self::getTeams(true); } - function create($vars,&$errors) { - return self::save(0,$vars,$errors); + function create($vars, &$errors) { + return self::save(0, $vars, $errors); } - function save($id,$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) { + } elseif(strlen($vars['name'])<3) { $errors['name']='Team name must be at least 3 chars.'; - }elseif(($tid=Team::getIdByName($vars['name'])) && $tid!=$id){ + } elseif(($tid=Team::getIdByName($vars['name'])) && $tid!=$id) { $errors['name']='Team name already exists'; } @@ -242,7 +246,7 @@ class Team { return true; $errors['err']='Unable to update the team. Internal error'; - }else{ + } else { $sql='INSERT INTO '.TEAM_TABLE.' '.$sql.',created=NOW()'; if(db_query($sql) && ($id=db_insert_id())) return $id; diff --git a/include/class.template.php b/include/class.template.php index fa8d070d544c4bf5eda0da758fed814f106c529d..8ff24503d8ac149bc658f02decfe9c8f0d029b39 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -100,6 +100,9 @@ class Template { case 'ticket_autoresp': $tpl=array('subj'=>$this->ht['ticket_autoresp_subj'],'body'=>$this->ht['ticket_autoresp_body']); break; + case 'ticket_autoreply': + $tpl=array('subj'=>$this->ht['ticket_autoreply_subj'],'body'=>$this->ht['ticket_autoreply_body']); + break; case 'msg_autoresp': $tpl=array('subj'=>$this->ht['message_autoresp_subj'],'body'=>$this->ht['message_autoresp_body']); break; @@ -159,6 +162,10 @@ class Template { return $this->getMsgTemplate('ticket_autoresp'); } + function getAutoReplyMsgTemplate() { + return $this->getMsgTemplate('ticket_autoreply'); + } + function getReplyMsgTemplate() { return $this->getMsgTemplate('ticket_reply'); } @@ -202,6 +209,9 @@ class Template { case 'ticket_autoresp': $sql.=',ticket_autoresp_subj='.db_input($vars['subj']).',ticket_autoresp_body='.db_input($vars['body']); break; + case 'ticket_autoreply': + $sql.=',ticket_autoreply_subj='.db_input($vars['subj']).',ticket_autoreply_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; @@ -283,8 +293,10 @@ class Template { function message_templates(){ //TODO: Make it database driven and dynamic - $messages=array('ticket_autoresp'=>array('name'=>'New Ticket Autoresponse', + $messages=array('ticket_autoresp'=>array('name'=>'New Ticket Auto-response', 'desc'=>'Autoresponse sent to user, if enabled, on new ticket.'), + 'ticket_autoreply'=>array('name'=>'New Ticket Auto-reply', + 'desc'=>'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.'), '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', @@ -365,6 +377,8 @@ class Template { .' ,cfg_id='.db_input($ost->getConfigId()) .' ,ticket_autoresp_subj='.db_input($info['ticket_autoresp_subj']) .' ,ticket_autoresp_body='.db_input($info['ticket_autoresp_body']) + .' ,ticket_autoreply_subj='.db_input($info['ticket_autoreply_subj']) + .' ,ticket_autoreply_body='.db_input($info['ticket_autoreply_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']) @@ -375,6 +389,8 @@ class Template { .' ,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']) + .' ,transfer_alert_subj='.db_input($info['transfer_alert_subj']) + .' ,transfer_alert_body='.db_input($info['transfer_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']) diff --git a/include/class.thread.php b/include/class.thread.php new file mode 100644 index 0000000000000000000000000000000000000000..d4daaf4b098cabcda7907ec26ea55a2bbd6b2976 --- /dev/null +++ b/include/class.thread.php @@ -0,0 +1,241 @@ +<?php +/********************************************************************* + class.thread.php + + Ticket thread + TODO: move thread related logic here from class.ticket.php + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.ticket.php'); + +Class ThreadEntry { + + var $id; + var $ht; + + var $staff; + var $ticket; + + function ThreadEntry($id, $type='', $ticketId=0) { + $this->load($id, $type, $ticketId); + } + + function load($id=0, $type='', $ticketId=0) { + + if(!$id && !($id=$this->getId())) + return false; + + $sql='SELECT thread.* ' + .' ,count(DISTINCT attach.attach_id) as attachments ' + .' FROM '.TICKET_THREAD_TABLE.' thread ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (thread.ticket_id=attach.ticket_id + AND thread.id=attach.ref_id + AND thread.thread_type=attach.ref_type) ' + .' WHERE thread.id='.db_input($id); + + if($type) + $sql.=' AND thread.thread_type='.db_input($type); + + if($ticketId) + $sql.=' AND thread.ticket_id='.db_input($ticketId); + + $sql.=' GROUP BY thread.id '; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht = db_fetch_array($res); + $this->id = $this->ht['id']; + + $this->staff = $this->ticket = null; + + return true; + } + + function reload() { + return $this->load(); + } + + function getId() { + return $this->id; + } + + function getPid() { + return $this->ht['pid']; + } + + function getType() { + return $this->ht['thread_type']; + } + + function getSource() { + return $this->ht['source']; + } + + function getPoster() { + return $this->ht['poster']; + } + + function getTitle() { + return $this->ht['title']; + } + + function getBody() { + return $this->ht['body']; + } + + function getCreateDate() { + return $this->ht['created']; + } + + function getUpdateDate() { + return $this->ht['updated']; + } + + function getNumAttachments() { + return $this->ht['attachments']; + } + + function getTicketId() { + return $this->ht['ticket_id']; + } + + function getTicket() { + + if(!$this->ticket && $this->getTicketId()) + $this->ticket = Ticket::lookup($this->getTicketId()); + + return $this->ticket; + } + + function getStaffId() { + return $this->ht['staff_id']; + } + + function getStaff() { + + if(!$this->staff && $this->getStaffId()) + $this->staff = Staff::lookup($this->getStaffId()); + + return $this->staff; + } + + /* variables */ + + function asVar() { + return $this->getBody(); + } + + function getVar($tag) { + global $cfg; + + if($tag && is_callable(array($this, 'get'.ucfirst($tag)))) + return call_user_func(array($this, 'get'.ucfirst($tag))); + + switch(strtolower($tag)) { + case 'create_date': + return Format::date( + $cfg->getDateTimeFormat(), + Misc::db2gmtime($this->getCreateDate()), + $cfg->getTZOffset(), + $cfg->observeDaylightSaving()); + break; + case 'update_date': + return Format::date( + $cfg->getDateTimeFormat(), + Misc::db2gmtime($this->getUpdateDate()), + $cfg->getTZOffset(), + $cfg->observeDaylightSaving()); + break; + } + + return false; + } + + /* static calls */ + + function lookup($id, $tid=0, $type='') { + return ($id + && is_numeric($id) + && ($e = new ThreadEntry($id, $type, $tid)) + && $e->getId()==$id + )?$e:null; + } +} + +/* Message - Ticket thread entry of type message */ +class Message extends ThreadEntry { + + function Message($id, $ticketId=0) { + parent::ThreadEntry($id, 'M', $ticketId); + } + + function getSubject() { + return $this->getTitle(); + } + + function lookup($id, $tid, $type='M') { + + return ($id + && is_numeric($id) + && ($m = new Message($id, $tid)) + && $m->getId()==$id + )?$m:null; + } +} + +/* Response - Ticket thread entry of type response */ +class Response extends ThreadEntry { + + function Response($id, $ticketId=0) { + parent::ThreadEntry($id, 'R', $ticketId); + } + + function getSubject() { + return $this->getTitle(); + } + + function getRespondent() { + return $this->getStaff(); + } + + function lookup($id, $tid, $type='R') { + + return ($id + && is_numeric($id) + && ($r = new Response($id, $tid)) + && $r->getId()==$id + )?$r:null; + } +} + +/* Note - Ticket thread entry of type note (Internal Note) */ +class Note extends ThreadEntry { + + function Note($id, $ticketId=0) { + parent::ThreadEntry($id, 'N', $ticketId); + } + + function getMessage() { + return $this->getBody(); + } + + function lookup($id, $tid, $type='N') { + + return ($id + && is_numeric($id) + && ($n = new Note($id, $tid)) + && $n->getId()==$id + )?$n:null; + } +} +?> diff --git a/include/class.ticket.php b/include/class.ticket.php index 8b9581d532a1e94c8076d3eca0b82bcfe88d7d0e..50e8173611715153187190c253ea98aec2a77759 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -13,6 +13,7 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +include_once(INCLUDE_DIR.'class.thread.php'); include_once(INCLUDE_DIR.'class.staff.php'); include_once(INCLUDE_DIR.'class.client.php'); include_once(INCLUDE_DIR.'class.team.php'); @@ -25,10 +26,12 @@ include_once(INCLUDE_DIR.'class.attachment.php'); include_once(INCLUDE_DIR.'class.pdf.php'); include_once(INCLUDE_DIR.'class.banlist.php'); include_once(INCLUDE_DIR.'class.template.php'); +include_once(INCLUDE_DIR.'class.variable.php'); include_once(INCLUDE_DIR.'class.priority.php'); include_once(INCLUDE_DIR.'class.sla.php'); +include_once(INCLUDE_DIR.'class.canned.php'); -class Ticket{ +class Ticket { var $id; var $extid; @@ -198,13 +201,17 @@ class Ticket{ } //Getters - function getId(){ + function getId() { return $this->id; } - function getExtId(){ + function getExtId() { return $this->extid; } + + function getNumber() { + return $this->getExtId(); + } function getEmail(){ return $this->email; @@ -419,6 +426,11 @@ class Ticket{ return $assignees; } + function getAssigned($glue='/') { + $assignees = $this->getAssignees(); + return $assignees?implode($glue, $assignees):''; + } + function getTopicId() { return $this->topic_id; } @@ -439,7 +451,7 @@ class Ticket{ function getSLA() { if(!$this->sla && $this->getSLAId()) - $this->sla = SLA::lookup($this->getSLAId); + $this->sla = SLA::lookup($this->getSLAId()); return $this->sla; } @@ -535,7 +547,7 @@ class Ticket{ } function getResponses($msgId=0) { - return $this->getThreadByType('R', $msgID); + return $this->getThreadByType('R', $msgId); } function getNotes() { @@ -656,9 +668,9 @@ class Ticket{ //DeptId can NOT be 0. No orphans please! function setDeptId($deptId){ - + //Make sure it's a valid department// - if(!($dept=Dept::lookup($deptId))) + if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId()) return false; @@ -669,16 +681,18 @@ class Ticket{ } //Set staff ID...assign/unassign/release (id can be 0) - function setStaffId($staffId){ + function setStaffId($staffId) { + + if(!is_numeric($staffId)) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId) .' WHERE ticket_id='.db_input($this->getId()); - if (db_query($sql) && db_affected_rows()) { - $this->staff_id = $staffId; - return true; - } - return false; + if (!db_query($sql) || !db_affected_rows()) + return false; + + $this->staff_id = $staffId; + return true; } function setSLAId($slaId) { @@ -708,23 +722,26 @@ class Ticket{ # email filter? This method doesn't consider such a case if ($trump !== null) { $slaId = $trump; - } elseif ($this->getDept()->getSLAId()) { + } elseif ($this->getDept() && $this->getDept()->getSLAId()) { $slaId = $this->getDept()->getSLAId(); - } elseif ($this->getTopicId() && $this->getTopic()) { + } elseif ($this->getTopic() && $this->getTopic()->getSLAId()) { $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()); + function setTeamId($teamId) { + + if(!is_numeric($teamId)) return false; + + $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()); + return (db_query($sql) && db_affected_rows()); } //Status helper. @@ -763,6 +780,11 @@ class Ticket{ case 'overdue': return $this->markOverdue(); break; + case 'notdue': + return $this->clearOverdue(); + break; + case 'unassined': + return $this->unassign(); } return false; @@ -783,15 +805,19 @@ class Ticket{ function close(){ global $thisstaff; - $sql='UPDATE '.TICKET_TABLE.' SET closed=NOW(), isoverdue=0, duedate=NULL, updated=NOW(), status='.db_input('closed'); - + $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()); + if(!db_query($sql) || !db_affected_rows()) + return false; + + $this->reload(); $this->logEvent('closed'); - return (db_query($sql) && db_affected_rows()); + + return true; } //set status to open on a closed ticket. @@ -832,17 +858,17 @@ class Ticket{ 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); + $msg = $this->replaceVars($msg, + array('message' => $message, + 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'') + ); + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $body ="\n$tag\n\n".$body; + $msg['body'] ="\n$tag\n\n".$msg['body']; //TODO: add auto flags....be nice to mail servers and sysadmins!! - $email->send($this->getEmail(),$subj,$body); + $email->send($this->getEmail(), $msg['subj'], $msg['body']); } if(!($email=$cfg->getAlertEmail())) @@ -852,17 +878,14 @@ class Ticket{ if($alertstaff && $email && $cfg->alertONNewTicket() && ($msg=$tpl->getNewTicketAlertMsgTemplate())) { - - $body=$this->replaceTemplateVars($msg['body']); - $subj=$this->replaceTemplateVars($msg['subj']); - $body = str_replace('%message', $message, $body); - + + $msg = $this->replaceVars($msg, array('message' => $message)); + $recipients=$sentlist=array(); - //Alert admin?? if($cfg->alertAdminONNewTicket()) { - $alert = str_replace("%staff",'Admin',$body); - $email->send($cfg->getAdminEmail(),$subj,$alert); + $alert = str_replace('%{recipient}', 'Admin', $msg['body']); + $email->send($cfg->getAdminEmail(), $msg['subj'], $alert); $sentlist[]=$cfg->getAdminEmail(); } @@ -877,8 +900,8 @@ class Ticket{ 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); + $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } @@ -907,20 +930,21 @@ class Ticket{ $email=$cfg->getDefaultEmail(); if($tpl && ($msg=$tpl->getOverlimitMsgTemplate()) && $email) { - $body=$this->replaceTemplateVars($msg['body']); - $subj=$this->replaceTemplateVars($msg['subj']); - $body = str_replace('%signature',($dept && $dept->isPublic())?$dept->getSignature():'',$body); - $email->send($this->getEmail(), $subj, $body); + + $msg = $this->replaceVars($msg, + array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); + + $email->send($this->getEmail(), $msg['subj'], $msg['body']); } $client= $this->getClient(); //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think. - $msg='Max. open tickets reached for '.$this->getEmail()."\n" - .'Open ticket: '.$client->getNumOpenTickets()."\n" - .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user."; + $alert='Max. open tickets reached for '.$this->getEmail()."\n" + .'Open ticket: '.$client->getNumOpenTickets()."\n" + .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user."; - $ost->alertAdmin('Overlimit Notice', $msg); + $ost->alertAdmin('Overlimit Notice', $alert); return true; } @@ -958,38 +982,40 @@ class Ticket{ if(!$dept || !($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - + $tpl = $cfg->getDefaultTemplate(); + + if(!$dept || !($email = $dept->getAutoRespEmail())) + $email = $cfg->getDefaultEmail(); + //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); + if($email && $tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { + + $msg = $this->replaceVars($msg, + array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); //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); - } + $msg['body'] ="\n$tag\n\n".$msg['body']; + + $email->send($this->getEmail(), $msg['subj'], $msg['body']); } } - function onAssign($note, $alert=true) { + function onAssign($assignee, $comments, $alert=true) { global $cfg, $thisstaff; if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign? + //Assignee must be an object of type Staff or Team + if(!$assignee || !is_object($assignee)) return false; + $this->reload(); + $comments = $comments?$comments:'Ticket assignment'; + $assigner = $thisstaff?$thisstaff:'SYSTEM (Auto Assignment)'; + //Log an internal note - no alerts on the internal note. - $note=$note?$note:'Ticket assignment'; - $this->postNote('Ticket Assigned to '.$this->getAssignee(),$note,false); + $this->logNote('Ticket Assigned to '.$assignee->getName(), $comments, $assigner, false); //See if we need to send alerts if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! @@ -998,39 +1024,39 @@ class Ticket{ //Get template. if(!$dept || !($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); + $tpl = $cfg->getDefaultTemplate(); //Email to use! if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); + $email = $cfg->getDefaultEmail(); + + //recipients + $recipients=array(); + if(!strcasecmp(get_class($assignee), 'Staff')) { + if($cfg->alertStaffONAssignment()) + $recipients[] = $assignee; + } elseif(!strcasecmp(get_class($assignee), 'Team')) { + if($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers())) + $recipients+=$members; + elseif($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead())) + $recipients[] = $lead; + } //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->alertStaffONAssignment() && $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; - } + if($email && $recipients && $tpl && ($msg=$tpl->getAssignedAlertMsgTemplate())) { + + $msg = $this->replaceVars($msg, + array('comments' => $comments, + 'assignee' => $assignee, + 'assigner' => $assigner + )); + //Send the alerts. $sentlist=array(); - foreach( $recipients as $k=>$staff){ + 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); + $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } } @@ -1059,10 +1085,8 @@ class Ticket{ //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. + + $msg = $this->replaceVars($msg, array('comments' => $comments)); //recipients $recipients=array(); @@ -1084,51 +1108,83 @@ class Ticket{ $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); + $alert = str_replace("%{recipient}", $staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } } return true; + + } + + //ticket obj as variable = ticket number. + function asVar() { + return $this->getNumber(); } - //Replace base variables. - function replaceTemplateVars($text){ + function getVar($tag) { 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/', - '/%auth/', '/%clientlink/'); - $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(), - $this->getAuthToken(), - '%url/view.php?t=%ticket&e=%email&a=%auth'); - while ($text != ($T = preg_replace($search,$replace,$text))) { - $text = $T; + if($tag && is_callable(array($this, 'get'.ucfirst($tag)))) + return call_user_func(array($this, 'get'.ucfirst($tag))); + + switch(strtolower($tag)) { + case 'phone_number': + return $this->getPhoneNumber(); + break; + case 'auth_token': + return $this->getAuthToken(); + break; + case 'client_link': + return sprintf('%s/view.php?t=%s&e=%s&a=%s', + $cfg->getBaseUrl(), $this->getNumber(), $this->getEmail(), $this->getAuthToken()); + break; + case 'staff_link': + return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId()); + break; + case 'create_date': + return Format::date( + $cfg->getDateTimeFormat(), + Misc::db2gmtime($this->getCreateDate()), + $cfg->getTZOffset(), + $cfg->observeDaylightSaving()); + break; + case 'due_date': + $duedate =''; + if($this->getDueDate()) + $duedate = Format::date( + $cfg->getDateTimeFormat(), + Misc::db2gmtime($this->getDueDate()), + $cfg->getTZOffset(), + $cfg->observeDaylightSaving()); + + return $duedate; + break; + case 'close_date'; + $closedate =''; + if($this->isClosed()) + $duedate = Format::date( + $cfg->getDateTimeFormat(), + Misc::db2gmtime($this->getCloseDate()), + $cfg->getTZOffset(), + $cfg->observeDaylightSaving()); + + return $closedate; + break; } - return $text; + + return false; + } + + //Replace base variables. + function replaceVars($input, $vars = array()) { + global $ost; + + $vars = array_merge($vars, array('ticket' => $this)); + + return $ost->replaceTemplateVariables($input, $vars); } function markUnAnswered() { @@ -1158,26 +1214,50 @@ class Ticket{ return true; } + function clearOverdue() { + + if(!$this->isOverdue()) + return true; + + $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() '; + //clear due date if it's in the past + if($this->getDueDate() && strtotime($this->getDueDate())<=time()) + $sql.=', duedate=NULL'; + + $sql.=' WHERE ticket_id='.db_input($this->getId()); + + return (db_query($sql) && db_affected_rows()); + } + //Dept Tranfer...with alert.. done by staff function transfer($deptId, $comments, $alert = true) { + global $cfg, $thisstaff; - if(!$this->setDeptId($deptId)) + if(!$thisstaff || !$thisstaff->canTransferTickets()) return false; - + + $currentDept = $this->getDeptName(); //Current department + + if(!$deptId || !$this->setDeptId($deptId)) + return false; + + // Reopen ticket if closed + if($this->isClosed()) $this->reopen(); + + $this->reload(); // Change to SLA of the new department $this->selectSLAId(); - $currentDept = $this->getDeptName(); //XXX: add to olddept to tpl vars?? + + /*** log the transfer comments as internal note - with alerts disabled - ***/ + $title='Ticket transfered from '.$currentDept.' to '.$this->getDeptName(); + $comments=$comments?$comments:$title; + $this->logNote($title, $comments, $thisstaff, false); - // Reopen ticket if closed - if($this->isClosed()) - $this->reopen(); + $this->logEvent('transferred'); - $this->reload(); //reload - new dept!! - $this->logEvent('transferred'); - - //Send out alerts if enabled AND requested - if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!! + //Send out alerts if enabled AND requested + if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!! //Get template. @@ -1191,10 +1271,7 @@ class Ticket{ //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); - + $msg = $this->replaceVars($msg, array('comments' => $comments, 'staff' => $thisstaff)); //recipients $recipients=array(); //Assigned staff or team... if any @@ -1216,8 +1293,8 @@ class Ticket{ $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); + $alert = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } } @@ -1233,7 +1310,7 @@ class Ticket{ if(!$this->setStaffId($staff->getId())) return false; - $this->onAssign($note, $alert); + $this->onAssign($staff, $note, $alert); $this->logEvent('assigned'); return true; @@ -1252,7 +1329,7 @@ class Ticket{ if($this->isClosed()) $this->setStaffId(0); - $this->onAssign($note, $alert); + $this->onAssign($team, $note, $alert); $this->logEvent('assigned'); return true; @@ -1267,7 +1344,7 @@ class Ticket{ 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!!! + $alert=($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); } @@ -1281,15 +1358,23 @@ class Ticket{ 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); + //We can only unassigned OPEN tickets. + if($this->isClosed()) + return false; - return false; - } + //Unassign staff (if any) + if($this->getStaffId() && !$this->setStaffId(0)) + return false; + + //unassign team (if any) + if($this->getTeamId() && !$this->setTeamId(0)) + return false; + $this->reload(); + + return true; + } + function release() { return $this->unassign(); } @@ -1331,7 +1416,7 @@ class Ticket{ if($newticket) return $msgid; //Our work is done... $autorespond = true; - if ($autorespond && $headers && EmailFilter::isAutoResponse(Mail_Parse::splitHeaders($headers))) + if ($autorespond && $headers && TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($headers))) $autorespond=false; $this->onMessage($autorespond); //must be called b4 sending alerts to staff. @@ -1347,9 +1432,7 @@ class Ticket{ //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", $message,$body); + $msg = $this->replaceVars($msg, array('message' => $message)); //Build list of recipients and fire the alerts. $recipients=array(); @@ -1369,8 +1452,8 @@ class Ticket{ $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); + $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } } @@ -1378,11 +1461,57 @@ class Ticket{ return $msgid; } - /* public */ - function postReply($vars, $errors, $alert = true) { - global $thisstaff,$cfg; + function postCannedReply($canned, $msgId, $alert=true) { + global $ost, $cfg; + + if((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled()) + return false; - if(!$thisstaff || !$thisstaff->isStaff() || !$cfg) return 0; + $files = array(); + foreach ($canned->getAttachments() as $file) + $files[] = $file['id']; + + $info = array('msgId' => $msgId, + 'response' => $this->replaceVars($canned->getResponse()), + 'cannedattachments' => $files); + + if(!($respId=$this->postReply($info, $errors, false))) + return false; + + $this->markUnAnswered(); + + if(!$alert) return $respId; + + $dept = $this->getDept(); + + if(!($tpl = $dept->getTemplate())) + $tpl= $cfg->getDefaultTemplate(); + + if(!$dept || !($email=$dept->getEmail())) + $email = $cfg->getDefaultEmail(); + + if($tpl && ($msg=$tpl->getAutoReplyMsgTemplate()) && $email) { + + if($dept && $dept->isPublic()) + $signature=$dept->getSignature(); + else + $signature=''; + + $msg = $this->replaceVars($msg, array('response' => $info['response'], 'signature' => $signature)); + + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $msg['body'] ="\n$tag\n\n".$msg['body']; + + $attachments =($cfg->emailAttachments() && $files)?$this->getAttachments($respId, 'R'):array(); + $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments); + } + + return $respId; + } + + /* public */ + function postReply($vars, &$errors, $alert = true) { + global $thisstaff, $cfg; if(!$vars['msgId']) $errors['msgId'] ='Missing messageId - internal error'; @@ -1391,14 +1520,16 @@ class Ticket{ if($errors) return 0; + $poster = $thisstaff?$thisstaff->getName():'SYSTEM (Canned Reply)'; + $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' .' ,thread_type="R"' .' ,ticket_id='.db_input($this->getId()) .' ,pid='.db_input($vars['msgId']) .' ,body='.db_input(Format::striptags($vars['response'])) - .' ,staff_id='.db_input($thisstaff->getId()) - .' ,poster='.db_input($thisstaff->getName()) - .' ,ip_address='.db_input($thisstaff->getIP()); + .' ,staff_id='.db_input($thisstaff?$thisstaff->getId():0) + .' ,poster='.db_input($poster) + .' ,ip_address='.db_input($thisstaff?$thisstaff->getIP():''); if(!db_query($sql) || !($respId=db_insert_id())) return false; @@ -1435,26 +1566,24 @@ class Ticket{ $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') + if($thisstaff && $vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) $signature=$dept->getSignature(); else $signature=''; - - $body = str_replace("%signature",$signature,$body); + + $msg = $this->replaceVars($msg, + array('response' => $vars['response'], 'signature' => $signature, 'staff' => $thisstaff)); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $body ="\n$tag\n\n".$body; + $msg['body'] ="\n$tag\n\n".$msg['body']; //Set attachments if emailing. $attachments =($cfg->emailAttachments() && $attachments)?$this->getAttachments($respId,'R'):array(); //TODO: setup 5 param (options... e.g mid trackable on replies) - $email->send($this->getEmail(), $subj, $body, $attachments); + $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments); } return $respId; @@ -1467,7 +1596,7 @@ class Ticket{ if(!$cfg || !$cfg->logTicketActivity()) return 0; - return $this->postNote($title,$note,false,'System'); + return $this->logNote($title, $note, 'SYSTEM', false); } // History log -- used for statistics generation (pretty reports) @@ -1498,26 +1627,63 @@ class Ticket{ && db_affected_rows() == 1; } - //Insert Internal Notes - function postNote($title,$note,$alert=true,$poster='') { - global $thisstaff,$cfg; - - $poster=($poster || !$thisstaff)?$poster:$thisstaff->getName(); + //Insert Internal Notes + function logNote($title, $note, $poster='SYSTEM', $alert=true) { + + return $this->postNote( + array('title' => $title, 'note' => $note), + $errors, + $poster, + $alert); + } + + function postNote($vars, &$errors, $poster, $alert=true) { + global $cfg, $thisstaff; + + if(!$vars || !is_array($vars)) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['note']) + $errors['note'] = 'Note required'; + + if($errors) return false; + $staffId = 0; + if($poster && is_object($poster)) { + $staffId = $poster->getId(); + $poster = $poster->getName(); + } elseif(!$poster) { + $poster ='SYSTEM'; + } + + //TODO: move to class.thread.php + $sql= 'INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '. ',thread_type="N"'. ',ticket_id='.db_input($this->getId()). - ',title='.db_input(Format::striptags($title)). - ',body='.db_input(Format::striptags($note)). - ',staff_id='.db_input($thisstaff?$thisstaff->getId():0). + ',title='.db_input(Format::striptags($vars['title']?$vars['title']:'[No Title]')). + ',body='.db_input(Format::striptags($vars['note'])). + ',staff_id='.db_input($staffId). ',poster='.db_input($poster); //echo $sql; if(!db_query($sql) || !($id=db_insert_id())) return false; + + //Upload attachments IF ANY - TODO: validate attachment types?? + if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + $attachments = $this->uploadAttachments($files, $id, 'N'); + + //Set state: Error on state change not critical! + if(isset($vars['state']) && $vars['state']) { + if($this->setState($vars['state'])) + $this->reload(); + } // If alerts are not enabled then return a success. if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept())) return $id; + + //Note obj. + $note = Note::lookup($id, $this->getId()); if(!($tpl = $dept->getTemplate())) $tpl= $cfg->getDefaultTemplate(); @@ -1527,12 +1693,8 @@ class Ticket{ 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); - # TODO: Support a variable replacement of the staff writing the - # note + + $msg = $this->replaceVars($msg, array('note' => $note)); // Alert recipients $recipients=array(); @@ -1549,12 +1711,13 @@ class Ticket{ if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId()) $recipients[]=$dept->getManager(); + $attachments =($attachments)?$this->getAttachments($id, 'N'):array(); $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); + if(in_array($staff->getEmail(),$sentlist) || ($staffId && $staffId==$staff->getId())) continue; + $alert = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']); + $email->send($staff->getEmail(), $msg['subj'], $alert, $attachments); $sentlist[] = $staff->getEmail(); } } @@ -1590,7 +1753,7 @@ class Ticket{ else $error ='Error #'.$file['error']; - $this->postNote('File Upload Error', $error, false); + $this->logNote('File Upload Error', $error, 'SYSTEM', false); $ost->logDebug('File Upload Error (Ticket #'.$this->getExtId().')', $error); } @@ -1657,15 +1820,15 @@ class Ticket{ $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); $fields['subject'] = array('type'=>'string', 'required'=>1, 'error'=>'Subject required'); $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Help topic required'); - $fields['slaId'] = array('type'=>'int', 'required'=>1, 'error'=>'SLA required'); $fields['priorityId'] = array('type'=>'int', 'required'=>1, 'error'=>'Priority required'); + $fields['slaId'] = array('type'=>'int', 'required'=>0, 'error'=>'Select SLA'); $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'); $fields['note'] = array('type'=>'text', 'required'=>1, 'error'=>'Reason for the update required'); if(!Validator::process($fields, $vars, $errors) && !$errors['err']) - $errors['err'] ='Missing or invalid data - check the errors and try again'; + $errors['err'] = 'Missing or invalid data - check the errors and try again'; if($vars['duedate']) { if($this->isClosed()) @@ -1711,7 +1874,7 @@ class Ticket{ if(!$vars['note']) $vars['note']=sprintf('Ticket Updated by %s', $thisstaff->getName()); - $this->postNote('Ticket Updated', $vars['note']); + $this->logNote('Ticket Updated', $vars['note'], $thisstaff); $this->reload(); return true; @@ -1719,8 +1882,17 @@ class Ticket{ /*============== Static functions. Use Ticket::function(params); ==================*/ - function getIdByExtId($extid) { - $sql ='SELECT ticket_id FROM '.TICKET_TABLE.' ticket WHERE ticketID='.db_input($extid); + function getIdByExtId($extId, $email=null) { + + if(!$extId || !is_numeric($extId)) + return 0; + + $sql ='SELECT ticket_id FROM '.TICKET_TABLE.' ticket ' + .' WHERE ticketID='.db_input($extId); + + if($email) + $sql.=' AND email='.db_input($email); + if(($res=db_query($sql)) && db_num_rows($res)) list($id)=db_fetch_row($res); @@ -1733,8 +1905,8 @@ class Ticket{ return ($id && is_numeric($id) && ($ticket= new Ticket($id)) && $ticket->getId()==$id)?$ticket:null; } - function lookupByExtId($id) { - return self::lookup(self:: getIdByExtId($id)); + function lookupByExtId($id, $email=null) { + return self::lookup(self:: getIdByExtId($id, $email)); } function genExtRandID() { @@ -1845,7 +2017,7 @@ class Ticket{ if ($vars['email'] && Validator::is_email($vars['email'])) { //Make sure the email address is not banned - if(EmailFilter::isBanned($vars['email'])) { + if(TicketFilter::isBanned($vars['email'])) { $errors['err']='Ticket denied. Error #403'; $ost->logWarning('Ticket denied', 'Banned email - '.$vars['email']); return 0; @@ -1865,12 +2037,15 @@ class Ticket{ return 0; } } + + //Init ticket filters... + $ticket_filter = new TicketFilter($origin, $vars); // Make sure email contents should not be rejected - if (($email_filter=new EmailFilter($vars)) - && ($filter=$email_filter->shouldReject())) { + if($ticket_filter + && ($filter=$ticket_filter->shouldReject())) { $errors['err']='Ticket denied. Error #403'; $ost->logWarning('Ticket denied', - sprintf('Banned email - %s by filter "%s"', + sprintf('Ticket rejected ( %s) by filter "%s"', $vars['email'], $filter->getName())); return 0; @@ -1915,7 +2090,7 @@ class Ticket{ } //Make sure the due date is valid - if($vars['duedate']){ + if($vars['duedate']) { if(!$vars['time'] || strpos($vars['time'],':')===false) $errors['time']='Select time'; elseif(strtotime($vars['duedate'].' '.$vars['time'])===false) @@ -1924,16 +2099,16 @@ class Ticket{ $errors['duedate']='Due date must be in the future'; } - # Perform email filter actions on the new ticket arguments XXX: Move filter to the top and check for reject... - if (!$errors && $email_filter) $email_filter->apply($vars); + //Any error above is fatal. + if($errors) return 0; + + # Perform ticket filter actions on the new ticket arguments + if ($ticket_filter) $ticket_filter->apply($vars); # Some things will need to be unpacked back into the scope of this # function if (isset($vars['autorespond'])) $autorespond=$vars['autorespond']; - //Any error above is fatal. - if($errors) return 0; - // OK...just do it. $deptId=$vars['deptId']; //pre-selected Dept if any. $priorityId=$vars['priorityId']; @@ -1945,17 +2120,24 @@ class Ticket{ $priorityId=$priorityId?$priorityId:$topic->getPriorityId(); if($autorespond) $autorespond=$topic->autoRespond(); $source=$vars['source']?$vars['source']:'Web'; + + //Auto assignment. + if (!isset($vars['staffId']) && $topic->getStaffId()) + $vars['staffId'] = $topic->getStaffId(); + elseif (!isset($vars['teamId']) && $topic->getTeamId()) + $vars['teamId'] = $topic->getTeamId(); + + //set default sla. + if(!isset($vars['slaId']) && $topic->getSLAId()) + $vars['slaId'] = $topic->getSLAId(); + }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(); @@ -2004,22 +2186,19 @@ class Ticket{ //Auto assign staff or team - auto assignment based on filter rules. if($vars['staffId'] && !$vars['assignId']) - $ticket->assignToStaff($vars['staffId'],'auto-assignment'); + $ticket->assignToStaff($vars['staffId'], 'Auto Assignment'); if($vars['teamId'] && !$vars['assignId']) - $ticket->assignToTeam($vars['teamId'],'auto-assignment'); + $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']))) { + TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($vars['header']))) { $autorespond=false; } @@ -2030,25 +2209,18 @@ class Ticket{ $autorespond=false; } + //post canned auto-response IF any (disables new ticket auto-response). if ($vars['cannedResponseId'] - && ($canned = Canned::lookup($vars['cannedResponseId'])) - && $canned->isEnabled()) { - $files = array(); - foreach ($canned->getAttachments() as $file) - $files[] = $file['id']; - $ticket->postReply( - array( - 'msgId' => $msgid, - 'response' => - $ticket->replaceTemplateVars($canned->getResponse()), - 'cannedattachments' => $files - ),$errors, true); - - // If a canned-response is immediately queued for this ticket, - // disable the autoresponse - $autorespond=false; + && $ticket->postCannedReply($vars['cannedResponseId'], $msgid, $autorespond)) { + $ticket->markUnAnswered(); //Leave the ticket as unanswred. + $autorespond = false; } + //Check department's auto response settings + // XXX: Dept. setting doesn't affect canned responses. + if($autorespond && $dept && !$dept->autoRespONNewTicket()) + $autorespond=false; + /***** See if we need to send some alerts ****/ $ticket->onNewTicket($vars['message'], $autorespond, $alertstaff); @@ -2088,7 +2260,7 @@ class Ticket{ // post response - if any if($vars['response']) { - $vars['response']=$ticket->replaceTemplateVars($vars['response']); + $vars['response'] = $ticket->replaceVars($vars['response']); if(($respId=$ticket->postReply($vars, $errors, false))) { //Only state supported is closed on response if(isset($vars['ticket_state']) && $thisstaff->canCloseTickets()) @@ -2097,16 +2269,16 @@ class Ticket{ } //Post Internal note if($vars['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team. - $ticket->assign($vars['assignId'],$vars['note']); + $ticket->assign($vars['assignId'], $vars['note']); } elseif($vars['note']) { //Not assigned...save optional note if any - $ticket->postNote('New Ticket',$vars['note'],false); + $ticket->logNote('New Ticket', $vars['note'], $thisstaff, 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'])) + if(!$cfg->notifyONNewStaffTicket() || !isset($vars['alertuser'])) return $ticket; //No alerts. //Send Notice to user --- if requested AND enabled!! @@ -2120,10 +2292,9 @@ class Ticket{ 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); + $message = $vars['issue']; + if($vars['response']) + $message.="\n\n".$vars['response']; if($vars['signature']=='mine') $signature=$thisstaff->getSignature(); @@ -2131,14 +2302,15 @@ class Ticket{ $signature=$dept->getSignature(); else $signature=''; - - $body = str_replace('%signature',$signature,$body); + + $msg = $ticket->replaceVars($msg, + array('message' => $message, 'signature' => $signature)); if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator()))) - $body ="\n$tag\n\n".$body; + $msg['body'] ="\n$tag\n\n".$msg['body']; - $attachments =($cfg->emailAttachments() && $respId)?$this->getAttachments($respId,'R'):array(); - $email->send($ticket->getEmail(), $subj, $body, $attachments); + $attachments =($cfg->emailAttachments() && $respId)?$ticket->getAttachments($respId,'R'):array(); + $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments); } return $ticket; @@ -2148,7 +2320,7 @@ class Ticket{ function checkOverdue() { $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 ' - .' JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id) ' + .' INNER JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) ' .' WHERE status=\'open\' AND isoverdue=0 ' .' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=T2.grace_period*3600) ' .' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=T2.grace_period*3600) ' diff --git a/include/class.topic.php b/include/class.topic.php index c8e6a9e72b9a8d26da82e97f48cfc9f25e97d51c..900ff76168363c47e9b4763fee6cf2f00334a0fd 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -48,6 +48,10 @@ class Topic { function reload() { return $this->load(); } + + function asVar() { + return $this->getName(); + } function getId() { return $this->id; diff --git a/include/class.upgrader.php b/include/class.upgrader.php index 7fa6c331ff542c44e7f4329c4e164f0848cf837e..012f88d1160bea1fb09d2104129d19e8fccebd3e 100644 --- a/include/class.upgrader.php +++ b/include/class.upgrader.php @@ -67,9 +67,9 @@ class Upgrader extends SetupWizard { $subject = 'Upgrader Error'; if($email) { - $email->send($thistaff->getEmail(), $subject, $error); + $email->send($thisstaff->getEmail(), $subject, $error); } else {//no luck - try the system mail. - Mailer::sendmail($thistaff->getEmail(), $subject, $error, sprintf('"osTicket Alerts"<%s>', $thistaff->getEmail())); + Mailer::sendmail($thisstaff->getEmail(), $subject, $error, sprintf('"osTicket Alerts"<%s>', $thisstaff->getEmail())); } } @@ -269,8 +269,6 @@ class Upgrader extends SetupWizard { $tasks=array(); switch($phash) { //Add patch specific scripted tasks. case 'c00511c7-7be60a84': //V1.6 ST- 1.7 * {{MD5('1.6 ST') -> c00511c7c1db65c0cfad04b4842afc57}} - $tasks[] = array('func' => 'migrateAttachments2DB', - 'desc' => 'Migrating attachments to database, it might take a while depending on the number of files.'); $tasks[] = array('func' => 'migrateSessionFile2DB', 'desc' => 'Transitioning to db-backed sessions'); break; @@ -282,6 +280,10 @@ class Upgrader extends SetupWizard { $tasks[] = array('func' => 'migrateGroupDeptAccess', 'desc' => 'Migrating group\'s department access to a new table'); break; + case '15b30765-dd0022fb': + $tasks[] = array('func' => 'migrateAttachments2DB', + 'desc' => 'Migrating attachments to database, it might take a while depending on the number of files.'); + break; } //Check IF SQL cleanup exists. diff --git a/include/class.validator.php b/include/class.validator.php index addbfac4df3be2333c986ea26e76cac5b43e5dce..ef3a21499e008c3b3c25d44fe55c950b2ebd5410 100644 --- a/include/class.validator.php +++ b/include/class.validator.php @@ -137,33 +137,13 @@ class Validator { } 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); + $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+!*(),;?&=\$_.-]+)?@)?"; # nolint - // HOSTNAME OR IP - // http://x = allowed (ex. http://localhost, http://routerlogin) - $urlregex .= "[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*"; # nolint - //$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+\$_-]\.?)+)*\/?"; # nolint - // GET Query (optional) - $urlregex .= "(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?"; # nolint - // ANCHOR (optional) - $urlregex .= "(#[a-z_.-][a-z0-9+\$_.-]*)?\$"; # nolint - - return eregi($urlregex, $url)?true:false; + function is_url($url) { + //XXX: parse_url is not ideal for validating urls but it's ideal for basic checks. + return ($url && ($info=parse_url($url)) && $info['host']); } function is_ip($ip) { diff --git a/include/class.variable.php b/include/class.variable.php new file mode 100644 index 0000000000000000000000000000000000000000..f83ba61be9063d385943b4fe4f4fdc751a58f422 --- /dev/null +++ b/include/class.variable.php @@ -0,0 +1,147 @@ +<?php +/********************************************************************* + class.variable.php + + Variable replacer + + Used to parse, resolve and replace variables. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class VariableReplacer { + + var $start_delim; + var $end_delim; + + var $objects; + var $variables; + + var $errors; + + function VariableReplacer($start_delim='%{', $end_delim='}') { + + $this->start_delim = $start_delim; + $this->end_delim = $end_delim; + + $this->objects = array(); + $this->variables = array(); + } + + function setError($error) { + $this->errors[] = $error; + } + + function getErrors() { + return $this->errors; + } + + function getObj($tag) { + return @$this->objects[$tag]; + } + + function assign($var, $val='') { + + if($val && is_object($val)) { + $this->objects[$var] = $val; + } elseif($var && is_array($var)) { + foreach($var as $k => $v) + $this->assign($k, $v); + } elseif($var) { + $this->variables[$var] = $val; + } + } + + function getVar($obj, $var) { + + if(!$obj) return ""; + + if(!$var && is_callable(array($obj, 'asVar'))) + return call_user_func(array($obj, 'asVar')); + + list($v, $part) = explode('.', $var, 2); + if($v && is_callable(array($obj, 'get'.ucfirst($v)))) { + $rv = call_user_func(array($obj, 'get'.ucfirst($v))); + if(!$rv || !is_object($rv)) + return $rv; + + return $this->getVar($rv, $part); + } + + if(!$var || !is_callable(array($obj, 'getVar'))) + return ""; + + $parts = explode('.', $var); + if(($rv = call_user_func(array($obj, 'getVar'), $parts[0]))===false) + return ""; + + if(!is_object($rv)) + return $rv; + + list(, $part) = explode('.', $var, 2); + + return $this->getVar($rv, $part); + } + + function replaceVars($input) { + + if($input && is_array($input)) + return array_map(array($this, 'replaceVars'), $input); + + if(!($vars=$this->_parse($input))) + return $input; + + return preg_replace($this->_delimit(array_keys($vars)), array_values($vars), $input); + } + + function _resolveVar($var) { + + //Variable already memoized? + if($var && @isset($this->variables[$var])) + return $this->variables[$var]; + + $parts = explode('.', $var, 2); + if($parts && ($obj=$this->getObj($parts[0]))) + return $this->getVar($obj, $parts[1]); + elseif($parts[0] && @isset($this->variables[$parts[0]])) //root overwrite + return $this->variables[$parts[0]]; + + //Unknown object or variable - leavig it alone. + $this->setError('Unknown obj for "'.$var.'" tag '); + return false; + } + + function _parse($text) { + + $input = $text; + if(!preg_match_all('/'.$this->start_delim.'([A-Za-z_][\w._]+)'.$this->end_delim.'/', $input, $result)) + return null; + + $vars = array(); + foreach($result[0] as $k => $v) { + if(isset($vars[$v])) continue; + $val=$this->_resolveVar($result[1][$k]); + if($val!==false) + $vars[$v] = $val; + } + + return $vars; + } + + //Helper function - will be replaced by a lambda function (PHP 5.3+) + function _delimit($val, $d='/') { + + if($val && is_array($val)) + return array_map(array($this, '_delimit'), $val); + + return $d.$val.$d; + } +} +?> diff --git a/include/client/knowledgebase.inc.php b/include/client/knowledgebase.inc.php index d8f402e7b5d191b224dc638b54f3a3177bddce85..2e22b417a373290b934acba8514f4e7042c952f8 100644 --- a/include/client/knowledgebase.inc.php +++ b/include/client/knowledgebase.inc.php @@ -63,7 +63,7 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']); if($_REQUEST['q']) - $sql.=" AND MATCH(question,answer,keywords) AGAINST ('".db_input($_REQUEST['q'],false)."')"; + $sql.=" AND question LIKE ('%".db_input($_REQUEST['q'],false)."%') OR answer LIKE ('%".db_input($_REQUEST['q'],false)."%') OR keywords LIKE ('%".db_input($_REQUEST['q'],false)."%')"; $sql.=' GROUP BY faq.faq_id'; echo "<div><strong>Search Results</strong></div><div class='clear'></div>"; diff --git a/include/client/login.inc.php b/include/client/login.inc.php index e1e52e9d454e7a1a4f62fdfec39c5625e496c5d2..7e46c2eb30598e2220128654b0232cd08d81ac62 100644 --- a/include/client/login.inc.php +++ b/include/client/login.inc.php @@ -1,5 +1,5 @@ <?php -if(!defined('OSTCLIENTINC')) die('Kwaheri'); +if(!defined('OSTCLIENTINC')) die('Access Denied'); $email=Format::input($_POST['lemail']?$_POST['lemail']:$_GET['e']); $ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']); @@ -8,7 +8,8 @@ $ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']); <p>To view the status of a ticket, provide us with the login details below.</p> <form action="login.php" method="post" id="clientLogin"> <?php csrf_token(); ?> - <strong>Authentication Required</strong> + <strong><?php echo Format::htmlchars($errors['login']); ?></strong> + <br> <div> <label for="email">E-Mail Address:</label> <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>"> diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 1ee781a908a4ba275ca734f1d62be5d684198b9a..7f1751872e3ec4d2db60f0855dd433edbafb2d3a 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -114,13 +114,13 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting <th width="80"> <a href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Status">Status</a> </th> - <th width="240"> + <th width="300"> <a href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Subject">Subject</a> </th> <th width="150"> <a href="tickets.php?sort=dept&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Department">Department</a> </th> - <th width="150">Phone Number</th> + <th width="100">Phone Number</th> </tr> </thead> <tbody> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index ccea4c927cfdf31793ae325f79572a743fa35f26..fa8a5b4a5420df21565f463b79c59ac33ecceca7 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -65,7 +65,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { //Making sure internal notes are not displayed due to backend MISTAKES! if(!$threadType[$entry['thread_type']]) continue; $poster = $entry['poster']; - if($entry['thread_type']=='R' && $cfg->hideStaffName()) + if($entry['thread_type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id'])) $poster = ' '; ?> <table class="<?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> diff --git a/include/mysql.php b/include/mysql.php index 65adb6298a632ebe0d8c0a127d2c0af8982bf546..7a3819f4e839f891b8dfb940f73d57209cc436d5 100644 --- a/include/mysql.php +++ b/include/mysql.php @@ -30,7 +30,8 @@ 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 NAMES "utf8"'); + @mysql_query('SET CHARACTER SET "utf8"'); @mysql_query('SET COLLATION_CONNECTION=utf8_general_ci'); @db_set_variable('sql_mode', ''); diff --git a/include/pear/Auth/SASL.php b/include/pear/Auth/SASL.php index 45a3f713d2e2444ed1718d7a40f2591c081209d4..5bd6eb09610106ed1d6315d83c0589754ebef6ac 100644 --- a/include/pear/Auth/SASL.php +++ b/include/pear/Auth/SASL.php @@ -1,99 +1,125 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: SASL.php,v 1.5 2006/03/22 05:20:11 amistry Exp $ - -/** -* Client implementation of various SASL mechanisms -* -* @author Richard Heyes <richard@php.net> -* @access public -* @version 1.0 -* @package Auth_SASL -*/ - -require_once('PEAR.php'); - -class Auth_SASL -{ - /** - * Factory class. Returns an object of the request - * type. - * - * @param string $type One of: Anonymous - * Plain - * CramMD5 - * DigestMD5 - * Types are not case sensitive - */ - function &factory($type) - { - switch (strtolower($type)) { - case 'anonymous': - $filename = 'Auth/SASL/Anonymous.php'; - $classname = 'Auth_SASL_Anonymous'; - break; - - case 'login': - $filename = 'Auth/SASL/Login.php'; - $classname = 'Auth_SASL_Login'; - break; - - case 'plain': - $filename = 'Auth/SASL/Plain.php'; - $classname = 'Auth_SASL_Plain'; - break; - - case 'crammd5': - $filename = 'Auth/SASL/CramMD5.php'; - $classname = 'Auth_SASL_CramMD5'; - break; - - case 'digestmd5': - $filename = 'Auth/SASL/DigestMD5.php'; - $classname = 'Auth_SASL_DigestMD5'; - break; - - default: - return PEAR::raiseError('Invalid SASL mechanism type'); - break; - } - - require_once($filename); - $obj = new $classname(); - return $obj; - } -} - -?> \ No newline at end of file +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Client implementation of various SASL mechanisms +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('PEAR.php'); + +class Auth_SASL +{ + /** + * Factory class. Returns an object of the request + * type. + * + * @param string $type One of: Anonymous + * Plain + * CramMD5 + * DigestMD5 + * SCRAM-* (any mechanism of the SCRAM family) + * 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 'external': + $filename = 'Auth/SASL/External.php'; + $classname = 'Auth_SASL_External'; + break; + + case 'crammd5': + // $msg = 'Deprecated mechanism name. Use IANA-registered name: CRAM-MD5.'; + // trigger_error($msg, E_USER_DEPRECATED); + case 'cram-md5': + $filename = 'Auth/SASL/CramMD5.php'; + $classname = 'Auth_SASL_CramMD5'; + break; + + case 'digestmd5': + // $msg = 'Deprecated mechanism name. Use IANA-registered name: DIGEST-MD5.'; + // trigger_error($msg, E_USER_DEPRECATED); + case 'digest-md5': + // $msg = 'DIGEST-MD5 is a deprecated SASL mechanism as per RFC-6331. Using it could be a security risk.'; + // trigger_error($msg, E_USER_NOTICE); + $filename = 'Auth/SASL/DigestMD5.php'; + $classname = 'Auth_SASL_DigestMD5'; + break; + + default: + $scram = '/^SCRAM-(.{1,9})$/i'; + if (preg_match($scram, $type, $matches)) + { + $hash = $matches[1]; + $filename = dirname(__FILE__) .'/SASL/SCRAM.php'; + $classname = 'Auth_SASL_SCRAM'; + $parameter = $hash; + break; + } + return PEAR::raiseError('Invalid SASL mechanism type'); + break; + } + + require_once($filename); + if (isset($parameter)) + $obj = new $classname($parameter); + else + $obj = new $classname(); + return $obj; + } +} + +?> diff --git a/include/pear/Auth/SASL/Anonymous.php b/include/pear/Auth/SASL/Anonymous.php index dc6511c1af248645664d969b0868f02e93b8bda3..08119093643e0468c23b00b551372ee9c2d251dd 100644 --- a/include/pear/Auth/SASL/Anonymous.php +++ b/include/pear/Auth/SASL/Anonymous.php @@ -1,71 +1,71 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: Anonymous.php,v 1.4 2003/02/21 16:07:17 mj Exp $ - -/** -* Implmentation of ANONYMOUS SASL mechanism -* -* @author Richard Heyes <richard@php.net> -* @access public -* @version 1.0 -* @package Auth_SASL -*/ - -require_once('Auth/SASL/Common.php'); - -class Auth_SASL_Anonymous extends Auth_SASL_Common -{ - /** - * Not much to do here except return the token supplied. - * No encoding, hashing or encryption takes place for this - * mechanism, simply one of: - * o An email address - * o An opaque string not containing "@" that can be interpreted - * by the sysadmin - * o Nothing - * - * We could have some logic here for the second option, but this - * would by no means create something interpretable. - * - * @param string $token Optional email address or string to provide - * as trace information. - * @return string The unaltered input token - */ - function getResponse($token = '') - { - return $token; - } -} +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of ANONYMOUS SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Anonymous extends Auth_SASL_Common +{ + /** + * Not much to do here except return the token supplied. + * No encoding, hashing or encryption takes place for this + * mechanism, simply one of: + * o An email address + * o An opaque string not containing "@" that can be interpreted + * by the sysadmin + * o Nothing + * + * We could have some logic here for the second option, but this + * would by no means create something interpretable. + * + * @param string $token Optional email address or string to provide + * as trace information. + * @return string The unaltered input token + */ + function getResponse($token = '') + { + return $token; + } +} ?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/Common.php b/include/pear/Auth/SASL/Common.php index f21c8cc1ce55ea7548e2a88ce96efb446ea715f7..d8c5610d1658eaafb0bcdcab018fabb96b99a307 100644 --- a/include/pear/Auth/SASL/Common.php +++ b/include/pear/Auth/SASL/Common.php @@ -1,38 +1,38 @@ <?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: Common.php,v 1.6 2003/02/21 16:07:17 mj Exp $ +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ /** * Common functionality to SASL mechanisms @@ -49,10 +49,12 @@ 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 + * @param string $data The data to hash + * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. + * + * @return string The HMAC-MD5 digest */ - function _HMAC_MD5($key, $data) + function _HMAC_MD5($key, $data, $raw_output = FALSE) { if (strlen($key) > 64) { $key = pack('H32', md5($key)); @@ -66,9 +68,38 @@ class Auth_SASL_Common $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); $inner = pack('H32', md5($k_ipad . $data)); - $digest = md5($k_opad . $inner); + $digest = md5($k_opad . $inner, $raw_output); return $digest; } + + /** + * Function which implements HMAC-SHA-1 digest + * + * @param string $key The secret key + * @param string $data The data to hash + * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. + * @return string The HMAC-SHA-1 digest + * @author Jehan <jehan.marmottard@gmail.com> + * @access protected + */ + protected function _HMAC_SHA1($key, $data, $raw_output = FALSE) + { + if (strlen($key) > 64) { + $key = sha1($key, TRUE); + } + + 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('H40', sha1($k_ipad . $data)); + $digest = sha1($k_opad . $inner, $raw_output); + + return $digest; + } } ?> diff --git a/include/pear/Auth/SASL/CramMD5.php b/include/pear/Auth/SASL/CramMD5.php index 086248fee8b6d6f63d1df55d7a44d308500bb1cd..d3fbf179b2b974295d97fb4bd4ba80cda52119f5 100644 --- a/include/pear/Auth/SASL/CramMD5.php +++ b/include/pear/Auth/SASL/CramMD5.php @@ -1,68 +1,68 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: CramMD5.php,v 1.4 2003/02/21 16:07:17 mj Exp $ - -/** -* Implmentation of CRAM-MD5 SASL mechanism -* -* @author Richard Heyes <richard@php.net> -* @access public -* @version 1.0 -* @package Auth_SASL -*/ - -require_once('Auth/SASL/Common.php'); - -class Auth_SASL_CramMD5 extends Auth_SASL_Common -{ - /** - * Implements the CRAM-MD5 SASL mechanism - * This DOES NOT base64 encode the return value, - * you will need to do that yourself. - * - * @param string $user Username - * @param string $pass Password - * @param string $challenge The challenge supplied by the server. - * this should be already base64_decoded. - * - * @return string The string to pass back to the server, of the form - * "<user> <digest>". This is NOT base64_encoded. - */ - function getResponse($user, $pass, $challenge) - { - return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); - } -} +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of CRAM-MD5 SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_CramMD5 extends Auth_SASL_Common +{ + /** + * Implements the CRAM-MD5 SASL mechanism + * This DOES NOT base64 encode the return value, + * you will need to do that yourself. + * + * @param string $user Username + * @param string $pass Password + * @param string $challenge The challenge supplied by the server. + * this should be already base64_decoded. + * + * @return string The string to pass back to the server, of the form + * "<user> <digest>". This is NOT base64_encoded. + */ + function getResponse($user, $pass, $challenge) + { + return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); + } +} ?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/DigestMD5.php b/include/pear/Auth/SASL/DigestMD5.php index e97e15daba0c01f2cc361ecffec3cd177fdb7ee0..07007b7c91f336eac1e5853dba1bae77904e99a8 100644 --- a/include/pear/Auth/SASL/DigestMD5.php +++ b/include/pear/Auth/SASL/DigestMD5.php @@ -32,7 +32,7 @@ // | Author: Richard Heyes <richard@php.net> | // +-----------------------------------------------------------------------+ // -// $Id: DigestMD5.php,v 1.8 2006/03/22 05:20:11 amistry Exp $ +// $Id$ /** * Implmentation of DIGEST-MD5 SASL mechanism @@ -186,7 +186,6 @@ class Auth_SASL_DigestMD5 extends Auth_SASL_Common } else { $str = ''; - mt_srand((double)microtime()*10000000); for ($i=0; $i<32; $i++) { $str .= chr(mt_rand(0, 255)); } diff --git a/include/pear/Auth/SASL/External.php b/include/pear/Auth/SASL/External.php new file mode 100644 index 0000000000000000000000000000000000000000..c5ae25e75d584a93248bb219a57c214bfcf5af80 --- /dev/null +++ b/include/pear/Auth/SASL/External.php @@ -0,0 +1,63 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2008 Christoph Schulz | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Christoph Schulz <develop@kristov.de> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of EXTERNAL SASL mechanism +* +* @author Christoph Schulz <develop@kristov.de> +* @access public +* @version 1.0.3 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_External extends Auth_SASL_Common +{ + /** + * Returns EXTERNAL response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string EXTERNAL Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid; + } +} +?> diff --git a/include/pear/Auth/SASL/Login.php b/include/pear/Auth/SASL/Login.php index bad9ab98c3d10e5869f50826f0463bc80fa428a6..918daeedd14bc380653ea0247e05dbbb8e427d37 100644 --- a/include/pear/Auth/SASL/Login.php +++ b/include/pear/Auth/SASL/Login.php @@ -1,65 +1,65 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: Login.php,v 1.4 2003/02/21 16:07:17 mj Exp $ - -/** -* This is technically not a SASL mechanism, however -* it's used by Net_Sieve, Net_Cyrus and potentially -* other protocols , so here is a good place to abstract -* it. -* -* @author Richard Heyes <richard@php.net> -* @access public -* @version 1.0 -* @package Auth_SASL -*/ - -require_once('Auth/SASL/Common.php'); - -class Auth_SASL_Login extends Auth_SASL_Common -{ - /** - * Pseudo SASL LOGIN mechanism - * - * @param string $user Username - * @param string $pass Password - * @return string LOGIN string - */ - function getResponse($user, $pass) - { - return sprintf('LOGIN %s %s', $user, $pass); - } -} +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* This is technically not a SASL mechanism, however +* it's used by Net_Sieve, Net_Cyrus and potentially +* other protocols , so here is a good place to abstract +* it. +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Login extends Auth_SASL_Common +{ + /** + * Pseudo SASL LOGIN mechanism + * + * @param string $user Username + * @param string $pass Password + * @return string LOGIN string + */ + function getResponse($user, $pass) + { + return sprintf('LOGIN %s %s', $user, $pass); + } +} ?> \ No newline at end of file diff --git a/include/pear/Auth/SASL/Plain.php b/include/pear/Auth/SASL/Plain.php index e4662ff700269d6eff59f509118a4f0b34c7130b..57894d04209494853e303158bfb050235f211b93 100644 --- a/include/pear/Auth/SASL/Plain.php +++ b/include/pear/Auth/SASL/Plain.php @@ -1,63 +1,63 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2002-2003 Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Author: Richard Heyes <richard@php.net> | -// +-----------------------------------------------------------------------+ -// -// $Id: Plain.php,v 1.6 2003/09/11 18:53:56 mbretter Exp $ - -/** -* Implmentation of PLAIN SASL mechanism -* -* @author Richard Heyes <richard@php.net> -* @access public -* @version 1.0 -* @package Auth_SASL -*/ - -require_once('Auth/SASL/Common.php'); - -class Auth_SASL_Plain extends Auth_SASL_Common -{ - /** - * Returns PLAIN response - * - * @param string $authcid Authentication id (username) - * @param string $pass Password - * @param string $authzid Autorization id - * @return string PLAIN Response - */ - function getResponse($authcid, $pass, $authzid = '') - { - return $authzid . chr(0) . $authcid . chr(0) . $pass; - } -} -?> +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of PLAIN SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Plain extends Auth_SASL_Common +{ + /** + * Returns PLAIN response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string PLAIN Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid . chr(0) . $authcid . chr(0) . $pass; + } +} +?> diff --git a/include/pear/Auth/SASL/SCRAM.php b/include/pear/Auth/SASL/SCRAM.php new file mode 100644 index 0000000000000000000000000000000000000000..cbca500e47fcb522f8929265971c84af0b3b64f2 --- /dev/null +++ b/include/pear/Auth/SASL/SCRAM.php @@ -0,0 +1,306 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2011 Jehan | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Jehan <jehan.marmottard@gmail.com | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implementation of SCRAM-* SASL mechanisms. +* SCRAM mechanisms have 3 main steps (initial response, response to the server challenge, then server signature +* verification) which keep state-awareness. Therefore a single class instanciation must be done and reused for the whole +* authentication process. +* +* @author Jehan <jehan.marmottard@gmail.com> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_SCRAM extends Auth_SASL_Common +{ + /** + * Construct a SCRAM-H client where 'H' is a cryptographic hash function. + * + * @param string $hash The name cryptographic hash function 'H' as registered by IANA in the "Hash Function Textual + * Names" registry. + * @link http://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xml "Hash Function Textual + * Names" + * format of core PHP hash function. + * @access public + */ + function __construct($hash) + { + // Though I could be strict, I will actually also accept the naming used in the PHP core hash framework. + // For instance "sha1" is accepted, while the registered hash name should be "SHA-1". + $hash = strtolower($hash); + $hashes = array('md2' => 'md2', + 'md5' => 'md5', + 'sha-1' => 'sha1', + 'sha1' => 'sha1', + 'sha-224' > 'sha224', + 'sha224' > 'sha224', + 'sha-256' => 'sha256', + 'sha256' => 'sha256', + 'sha-384' => 'sha384', + 'sha384' => 'sha384', + 'sha-512' => 'sha512', + 'sha512' => 'sha512'); + if (function_exists('hash_hmac') && isset($hashes[$hash])) + { + $this->hash = create_function('$data', 'return hash("' . $hashes[$hash] . '", $data, TRUE);'); + $this->hmac = create_function('$key,$str,$raw', 'return hash_hmac("' . $hashes[$hash] . '", $str, $key, $raw);'); + } + elseif ($hash == 'md5') + { + $this->hash = create_function('$data', 'return md5($data, true);'); + $this->hmac = array($this, '_HMAC_MD5'); + } + elseif (in_array($hash, array('sha1', 'sha-1'))) + { + $this->hash = create_function('$data', 'return sha1($data, true);'); + $this->hmac = array($this, '_HMAC_SHA1'); + } + else + return PEAR::raiseError('Invalid SASL mechanism type'); + } + + /** + * Provides the (main) client response for SCRAM-H. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The challenge sent by the server. + * If the challenge is NULL or an empty string, the result will be the "initial response". + * @param string $authzid Authorization id (username to proxy as) + * @return string|false The response (binary, NOT base64 encoded) + * @access public + */ + public function getResponse($authcid, $pass, $challenge = NULL, $authzid = NULL) + { + $authcid = $this->_formatName($authcid); + if (empty($authcid)) + { + return false; + } + if (!empty($authzid)) + { + $authzid = $this->_formatName($authzid); + if (empty($authzid)) + { + return false; + } + } + + if (empty($challenge)) + { + return $this->_generateInitialResponse($authcid, $authzid); + } + else + { + return $this->_generateResponse($challenge, $pass); + } + + } + + /** + * Prepare a name for inclusion in a SCRAM response. + * + * @param string $username a name to be prepared. + * @return string the reformated name. + * @access private + */ + private function _formatName($username) + { + // TODO: prepare through the SASLprep profile of the stringprep algorithm. + // See RFC-4013. + + $username = str_replace('=', '=3D', $username); + $username = str_replace(',', '=2C', $username); + return $username; + } + + /** + * Generate the initial response which can be either sent directly in the first message or as a response to an empty + * server challenge. + * + * @param string $authcid Prepared authentication identity. + * @param string $authzid Prepared authorization identity. + * @return string The SCRAM response to send. + * @access private + */ + private function _generateInitialResponse($authcid, $authzid) + { + $init_rep = ''; + $gs2_cbind_flag = 'n,'; // TODO: support channel binding. + $this->gs2_header = $gs2_cbind_flag . (!empty($authzid)? 'a=' . $authzid : '') . ','; + + // I must generate a client nonce and "save" it for later comparison on second response. + $this->cnonce = $this->_getCnonce(); + // XXX: in the future, when mandatory and/or optional extensions are defined in any updated RFC, + // this message can be updated. + $this->first_message_bare = 'n=' . $authcid . ',r=' . $this->cnonce; + return $this->gs2_header . $this->first_message_bare; + } + + /** + * Parses and verifies a non-empty SCRAM challenge. + * + * @param string $challenge The SCRAM challenge + * @return string|false The response to send; false in case of wrong challenge or if an initial response has not + * been generated first. + * @access private + */ + private function _generateResponse($challenge, $password) + { + // XXX: as I don't support mandatory extension, I would fail on them. + // And I simply ignore any optional extension. + $server_message_regexp = "#^r=([\x21-\x2B\x2D-\x7E]+),s=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?),i=([0-9]*)(,[A-Za-z]=[^,])*$#"; + if (!isset($this->cnonce, $this->gs2_header) + || !preg_match($server_message_regexp, $challenge, $matches)) + { + return false; + } + $nonce = $matches[1]; + $salt = base64_decode($matches[2]); + if (!$salt) + { + // Invalid Base64. + return false; + } + $i = intval($matches[3]); + + $cnonce = substr($nonce, 0, strlen($this->cnonce)); + if ($cnonce <> $this->cnonce) + { + // Invalid challenge! Are we under attack? + return false; + } + + $channel_binding = 'c=' . base64_encode($this->gs2_header); // TODO: support channel binding. + $final_message = $channel_binding . ',r=' . $nonce; // XXX: no extension. + + // TODO: $password = $this->normalize($password); // SASLprep profile of stringprep. + $saltedPassword = $this->hi($password, $salt, $i); + $this->saltedPassword = $saltedPassword; + $clientKey = call_user_func($this->hmac, $saltedPassword, "Client Key", TRUE); + $storedKey = call_user_func($this->hash, $clientKey, TRUE); + $authMessage = $this->first_message_bare . ',' . $challenge . ',' . $final_message; + $this->authMessage = $authMessage; + $clientSignature = call_user_func($this->hmac, $storedKey, $authMessage, TRUE); + $clientProof = $clientKey ^ $clientSignature; + $proof = ',p=' . base64_encode($clientProof); + + return $final_message . $proof; + } + + /** + * SCRAM has also a server verification step. On a successful outcome, it will send additional data which must + * absolutely be checked against this function. If this fails, the entity which we are communicating with is probably + * not the server as it has not access to your ServerKey. + * + * @param string $data The additional data sent along a successful outcome. + * @return bool Whether the server has been authenticated. + * If false, the client must close the connection and consider to be under a MITM attack. + * @access public + */ + public function processOutcome($data) + { + $verifier_regexp = '#^v=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?)$#'; + if (!isset($this->saltedPassword, $this->authMessage) + || !preg_match($verifier_regexp, $data, $matches)) + { + // This cannot be an outcome, you never sent the challenge's response. + return false; + } + + $verifier = $matches[1]; + $proposed_serverSignature = base64_decode($verifier); + $serverKey = call_user_func($this->hmac, $this->saltedPassword, "Server Key", true); + $serverSignature = call_user_func($this->hmac, $serverKey, $this->authMessage, TRUE); + return ($proposed_serverSignature === $serverSignature); + } + + /** + * Hi() call, which is essentially PBKDF2 (RFC-2898) with HMAC-H() as the pseudorandom function. + * + * @param string $str The string to hash. + * @param string $hash The hash value. + * @param int $i The iteration count. + * @access private + */ + private function hi($str, $salt, $i) + { + $int1 = "\0\0\0\1"; + $ui = call_user_func($this->hmac, $str, $salt . $int1, true); + $result = $ui; + for ($k = 1; $k < $i; $k++) + { + $ui = call_user_func($this->hmac, $str, $ui, true); + $result = $result ^ $ui; + } + return $result; + } + + + /** + * Creates the client nonce for the response + * + * @return string The cnonce value + * @access private + * @author Richard Heyes <richard@php.net> + */ + private function _getCnonce() + { + // TODO: I reused the nonce function from the DigestMD5 class. + // I should probably make this a protected function in Common. + if (@file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) { + return base64_encode(fread($fd, 32)); + + } elseif (@file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) { + return base64_encode(fread($fd, 32)); + + } else { + $str = ''; + for ($i=0; $i<32; $i++) { + $str .= chr(mt_rand(0, 255)); + } + + return base64_encode($str); + } + } + +} + +?> diff --git a/include/pear/BUNDLE b/include/pear/BUNDLE new file mode 100644 index 0000000000000000000000000000000000000000..22917abc2caad7086de5ea447757c18f2cd44321 --- /dev/null +++ b/include/pear/BUNDLE @@ -0,0 +1,13 @@ +Log of pear packages bundled with osTicket + +* osTicket v1.7.* +=========================================================== + PEAR-1.9.4 - core class + Mail-1.2.0 + Mail_Mime-1.8.5 + Net_SMTP-1.6.1 + Net_Socket-1.0.10 + Auth_SASL-1.0.6 + +-------- Changes ---------- +* Add connect() function to Mail/smtp.php diff --git a/include/pear/Mail.php b/include/pear/Mail.php index 7a3c70f82190e89749f7651b8d3b8b4fc4748681..75132ac2a6c3e9d99bd1784feb41154f0cd71d3d 100644 --- a/include/pear/Mail.php +++ b/include/pear/Mail.php @@ -1,245 +1,270 @@ -<?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Author: Chuck Hagenbuch <chuck@horde.org> | -// +----------------------------------------------------------------------+ -// -// $Id: Mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ - -require_once 'PEAR.php'; - -/** - * PEAR's Mail:: interface. Defines the interface for implementing - * mailers under the PEAR hierarchy, and provides supporting functions - * useful in multiple mailer backends. - * - * @access public - * @version $Revision: 1.20 $ - * @package Mail - */ -class Mail -{ - /** - * Line terminator used for separating header lines. - * @var string - */ - var $sep = "\r\n"; - - /** - * Provides an interface for generating Mail:: objects of various - * types - * - * @param string $driver The kind of Mail:: object to instantiate. - * @param array $params The parameters to pass to the Mail:: object. - * @return object Mail a instance of the driver class or if fails a PEAR Error - * @access public - */ - function &factory($driver, $params = array()) - { - $driver = strtolower($driver); - include_once 'Mail/' . $driver . '.php'; - $class = 'Mail_' . $driver; - if (class_exists($class)) { - $mailer = new $class($params); - return $mailer; - } else { - return PEAR::raiseError('Unable to find class for driver ' . $driver); - } - } - - /** - * Implements Mail::send() function using php's built-in mail() - * command. - * - * @param mixed $recipients Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. This may contain recipients not - * specified in the headers, for Bcc:, resending - * messages, etc. - * - * @param array $headers The array of headers to send with the mail, in an - * associative array, where the array key is the - * header name (ie, 'Subject'), and the array value - * is the header value (ie, 'test'). The header - * produced from those values would be 'Subject: - * test'. - * - * @param string $body The full text of the message body, including any - * Mime parts, etc. - * - * @return mixed Returns true on success, or a PEAR_Error - * containing a descriptive error message on - * failure. - * - * @access public - * @deprecated use Mail_mail::send instead - */ - function send($recipients, $headers, $body) - { - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } - - $result = $this->_sanitizeHeaders($headers); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - // if we're passed an array of recipients, implode it. - if (is_array($recipients)) { - $recipients = implode(', ', $recipients); - } - - // get the Subject out of the headers array so that we can - // pass it as a seperate argument to mail(). - $subject = ''; - if (isset($headers['Subject'])) { - $subject = $headers['Subject']; - unset($headers['Subject']); - } - - // flatten the headers out. - list(, $text_headers) = Mail::prepareHeaders($headers); - - return mail($recipients, $subject, $body, $text_headers); - } - - /** - * Sanitize an array of mail headers by removing any additional header - * strings present in a legitimate header's value. The goal of this - * filter is to prevent mail injection attacks. - * - * @param array $headers The associative array of headers to sanitize. - * - * @access private - */ - function _sanitizeHeaders(&$headers) - { - foreach ($headers as $key => $value) { - $headers[$key] = - preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', - null, $value); - } - } - - /** - * Take an array of mail headers and return a string containing - * text usable in sending a message. - * - * @param array $headers The array of headers to prepare, in an associative - * array, where the array key is the header name (ie, - * 'Subject'), and the array value is the header - * value (ie, 'test'). The header produced from those - * values would be 'Subject: test'. - * - * @return mixed Returns false if it encounters a bad address, - * otherwise returns an array containing two - * elements: Any From: address found in the headers, - * and the plain text version of the headers. - * @access private - */ - function prepareHeaders($headers) - { - $lines = array(); - $from = null; - - foreach ($headers as $key => $value) { - if (strcasecmp($key, 'From') === 0) { - include_once 'Mail/RFC822.php'; - $parser = new Mail_RFC822(); - $addresses = $parser->parseAddressList($value, 'localhost', false); - if (is_a($addresses, 'PEAR_Error')) { - return $addresses; - } - - $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; - - // Reject envelope From: addresses with spaces. - if (strstr($from, ' ')) { - return false; - } - - $lines[] = $key . ': ' . $value; - } elseif (strcasecmp($key, 'Received') === 0) { - $received = array(); - if (is_array($value)) { - foreach ($value as $line) { - $received[] = $key . ': ' . $line; - } - } - else { - $received[] = $key . ': ' . $value; - } - // Put Received: headers at the top. Spam detectors often - // flag messages with Received: headers after the Subject: - // as spam. - $lines = array_merge($received, $lines); - } else { - // If $value is an array (i.e., a list of addresses), convert - // it to a comma-delimited string of its elements (addresses). - if (is_array($value)) { - $value = implode(', ', $value); - } - $lines[] = $key . ': ' . $value; - } - } - - return array($from, join($this->sep, $lines)); - } - - /** - * Take a set of recipients and parse them, returning an array of - * bare addresses (forward paths) that can be passed to sendmail - * or an smtp server with the rcpt to: command. - * - * @param mixed Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. - * - * @return mixed An array of forward paths (bare addresses) or a PEAR_Error - * object if the address list could not be parsed. - * @access private - */ - function parseRecipients($recipients) - { - include_once 'Mail/RFC822.php'; - - // if we're passed an array, assume addresses are valid and - // implode them before parsing. - if (is_array($recipients)) { - $recipients = implode(', ', $recipients); - } - - // Parse recipients, leaving out all personal info. This is - // for smtp recipients, etc. All relevant personal information - // should already be in the headers. - $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false); - - // If parseAddressList() returned a PEAR_Error object, just return it. - if (is_a($addresses, 'PEAR_Error')) { - return $addresses; - } - - $recipients = array(); - if (is_array($addresses)) { - foreach ($addresses as $ob) { - $recipients[] = $ob->mailbox . '@' . $ob->host; - } - } - - return $recipients; - } - -} +<?php +/** + * PEAR's Mail:: interface. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2002-2007, Richard Heyes + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 1997-2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Mail.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +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: 294747 $ + * @package Mail + */ +class Mail +{ + /** + * Line terminator used for separating header lines. + * @var string + */ + var $sep = "\r\n"; + + /** + * Provides an interface for generating Mail:: objects of various + * types + * + * @param string $driver The kind of Mail:: object to instantiate. + * @param array $params The parameters to pass to the Mail:: object. + * @return object Mail a instance of the driver class or if fails a PEAR Error + * @access public + */ + function &factory($driver, $params = array()) + { + $driver = strtolower($driver); + @include_once 'Mail/' . $driver . '.php'; + $class = 'Mail_' . $driver; + if (class_exists($class)) { + $mailer = new $class($params); + return $mailer; + } else { + return PEAR::raiseError('Unable to find class for driver ' . $driver); + } + } + + /** + * Implements Mail::send() function using php's built-in mail() + * command. + * + * @param mixed $recipients Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * + * @param array $headers The array of headers to send with the mail, in an + * associative array, where the array key is the + * header name (ie, 'Subject'), and the array value + * is the header value (ie, 'test'). The header + * produced from those values would be 'Subject: + * test'. + * + * @param string $body The full text of the message body, including any + * Mime parts, etc. + * + * @return mixed Returns true on success, or a PEAR_Error + * containing a descriptive error message on + * failure. + * + * @access public + * @deprecated use Mail_mail::send instead + */ + function send($recipients, $headers, $body) + { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + // if we're passed an array of recipients, implode it. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // get the Subject out of the headers array so that we can + // pass it as a seperate argument to mail(). + $subject = ''; + if (isset($headers['Subject'])) { + $subject = $headers['Subject']; + unset($headers['Subject']); + } + + // flatten the headers out. + list(, $text_headers) = Mail::prepareHeaders($headers); + + return mail($recipients, $subject, $body, $text_headers); + } + + /** + * Sanitize an array of mail headers by removing any additional header + * strings present in a legitimate header's value. The goal of this + * filter is to prevent mail injection attacks. + * + * @param array $headers The associative array of headers to sanitize. + * + * @access private + */ + function _sanitizeHeaders(&$headers) + { + foreach ($headers as $key => $value) { + $headers[$key] = + preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', + null, $value); + } + } + + /** + * Take an array of mail headers and return a string containing + * text usable in sending a message. + * + * @param array $headers The array of headers to prepare, in an associative + * array, where the array key is the header name (ie, + * 'Subject'), and the array value is the header + * value (ie, 'test'). The header produced from those + * values would be 'Subject: test'. + * + * @return mixed Returns false if it encounters a bad address, + * otherwise returns an array containing two + * elements: Any From: address found in the headers, + * and the plain text version of the headers. + * @access private + */ + function prepareHeaders($headers) + { + $lines = array(); + $from = null; + + foreach ($headers as $key => $value) { + if (strcasecmp($key, 'From') === 0) { + include_once 'Mail/RFC822.php'; + $parser = new Mail_RFC822(); + $addresses = $parser->parseAddressList($value, 'localhost', false); + if (is_a($addresses, 'PEAR_Error')) { + return $addresses; + } + + $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; + + // Reject envelope From: addresses with spaces. + if (strstr($from, ' ')) { + return false; + } + + $lines[] = $key . ': ' . $value; + } elseif (strcasecmp($key, 'Received') === 0) { + $received = array(); + if (is_array($value)) { + foreach ($value as $line) { + $received[] = $key . ': ' . $line; + } + } + else { + $received[] = $key . ': ' . $value; + } + // Put Received: headers at the top. Spam detectors often + // flag messages with Received: headers after the Subject: + // as spam. + $lines = array_merge($received, $lines); + } else { + // If $value is an array (i.e., a list of addresses), convert + // it to a comma-delimited string of its elements (addresses). + if (is_array($value)) { + $value = implode(', ', $value); + } + $lines[] = $key . ': ' . $value; + } + } + + return array($from, join($this->sep, $lines)); + } + + /** + * Take a set of recipients and parse them, returning an array of + * bare addresses (forward paths) that can be passed to sendmail + * or an smtp server with the rcpt to: command. + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. + * + * @return mixed An array of forward paths (bare addresses) or a PEAR_Error + * object if the address list could not be parsed. + * @access private + */ + function parseRecipients($recipients) + { + include_once 'Mail/RFC822.php'; + + // if we're passed an array, assume addresses are valid and + // implode them before parsing. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); + } + + // Parse recipients, leaving out all personal info. This is + // for smtp recipients, etc. All relevant personal information + // should already be in the headers. + $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false); + + // If parseAddressList() returned a PEAR_Error object, just return it. + if (is_a($addresses, 'PEAR_Error')) { + return $addresses; + } + + $recipients = array(); + if (is_array($addresses)) { + foreach ($addresses as $ob) { + $recipients[] = $ob->mailbox . '@' . $ob->host; + } + } + + return $recipients; + } + +} diff --git a/include/pear/Mail/RFC822.php b/include/pear/Mail/RFC822.php index 866638268dad6d6254280b301d9057056cbffc62..58d36465cba21779887651cec4204f222f9271ec 100644 --- a/include/pear/Mail/RFC822.php +++ b/include/pear/Mail/RFC822.php @@ -1,940 +1,951 @@ -<?php -// +-----------------------------------------------------------------------+ -// | Copyright (c) 2001-2002, Richard Heyes | -// | All rights reserved. | -// | | -// | Redistribution and use in source and binary forms, with or without | -// | modification, are permitted provided that the following conditions | -// | are met: | -// | | -// | o Redistributions of source code must retain the above copyright | -// | notice, this list of conditions and the following disclaimer. | -// | o Redistributions in binary form must reproduce the above copyright | -// | notice, this list of conditions and the following disclaimer in the | -// | documentation and/or other materials provided with the distribution.| -// | o The names of the authors may not be used to endorse or promote | -// | products derived from this software without specific prior written | -// | permission. | -// | | -// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | -// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | -// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | -// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | -// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | -// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | -// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | -// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | -// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | -// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | -// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | -// | | -// +-----------------------------------------------------------------------+ -// | Authors: Richard Heyes <richard@phpguru.org> | -// | Chuck Hagenbuch <chuck@horde.org> | -// +-----------------------------------------------------------------------+ - -/** - * RFC 822 Email address list validation Utility - * - * What is it? - * - * This class will take an address string, and parse it into it's consituent - * parts, be that either addresses, groups, or combinations. Nested groups - * are not supported. The structure it returns is pretty straight forward, - * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use - * print_r() to view the structure. - * - * How do I use it? - * - * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;'; - * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) - * print_r($structure); - * - * @author Richard Heyes <richard@phpguru.org> - * @author Chuck Hagenbuch <chuck@horde.org> - * @version $Revision: 1.24 $ - * @license BSD - * @package Mail - */ -class Mail_RFC822 { - - /** - * The address being parsed by the RFC822 object. - * @var string $address - */ - var $address = ''; - - /** - * The default domain to use for unqualified addresses. - * @var string $default_domain - */ - var $default_domain = 'localhost'; - - /** - * Should we return a nested array showing groups, or flatten everything? - * @var boolean $nestGroups - */ - var $nestGroups = true; - - /** - * Whether or not to validate atoms for non-ascii characters. - * @var boolean $validate - */ - var $validate = true; - - /** - * The array of raw addresses built up as we parse. - * @var array $addresses - */ - var $addresses = array(); - - /** - * The final array of parsed address information that we build up. - * @var array $structure - */ - var $structure = array(); - - /** - * The current error message, if any. - * @var string $error - */ - var $error = null; - - /** - * An internal counter/pointer. - * @var integer $index - */ - var $index = null; - - /** - * The number of groups that have been found in the address list. - * @var integer $num_groups - * @access public - */ - var $num_groups = 0; - - /** - * A variable so that we can tell whether or not we're inside a - * Mail_RFC822 object. - * @var boolean $mailRFC822 - */ - var $mailRFC822 = true; - - /** - * A limit after which processing stops - * @var int $limit - */ - var $limit = null; - - /** - * Sets up the object. The address must either be set here or when - * calling parseAddressList(). One or the other. - * - * @access public - * @param string $address The address(es) to validate. - * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. - * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. - * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. - * - * @return object Mail_RFC822 A new Mail_RFC822 object. - */ - function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) - { - if (isset($address)) $this->address = $address; - if (isset($default_domain)) $this->default_domain = $default_domain; - if (isset($nest_groups)) $this->nestGroups = $nest_groups; - if (isset($validate)) $this->validate = $validate; - if (isset($limit)) $this->limit = $limit; - } - - /** - * Starts the whole process. The address must either be set here - * or when creating the object. One or the other. - * - * @access public - * @param string $address The address(es) to validate. - * @param string $default_domain Default domain/host etc. - * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. - * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. - * - * @return array A structured array of addresses. - */ - function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) - { - if (!isset($this) || !isset($this->mailRFC822)) { - $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); - return $obj->parseAddressList(); - } - - if (isset($address)) $this->address = $address; - if (isset($default_domain)) $this->default_domain = $default_domain; - if (isset($nest_groups)) $this->nestGroups = $nest_groups; - if (isset($validate)) $this->validate = $validate; - if (isset($limit)) $this->limit = $limit; - - $this->structure = array(); - $this->addresses = array(); - $this->error = null; - $this->index = null; - - // Unfold any long lines in $this->address. - $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); - $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); - - while ($this->address = $this->_splitAddresses($this->address)); - - if ($this->address === false || isset($this->error)) { - require_once 'PEAR.php'; - return PEAR::raiseError($this->error); - } - - // Validate each address individually. If we encounter an invalid - // address, stop iterating and return an error immediately. - foreach ($this->addresses as $address) { - $valid = $this->_validateAddress($address); - - if ($valid === false || isset($this->error)) { - require_once 'PEAR.php'; - return PEAR::raiseError($this->error); - } - - if (!$this->nestGroups) { - $this->structure = array_merge($this->structure, $valid); - } else { - $this->structure[] = $valid; - } - } - - return $this->structure; - } - - /** - * Splits an address into separate addresses. - * - * @access private - * @param string $address The addresses to split. - * @return boolean Success or failure. - */ - function _splitAddresses($address) - { - if (!empty($this->limit) && count($this->addresses) == $this->limit) { - return ''; - } - - if ($this->_isGroup($address) && !isset($this->error)) { - $split_char = ';'; - $is_group = true; - } elseif (!isset($this->error)) { - $split_char = ','; - $is_group = false; - } elseif (isset($this->error)) { - return false; - } - - // Split the string based on the above ten or so lines. - $parts = explode($split_char, $address); - $string = $this->_splitCheck($parts, $split_char); - - // If a group... - if ($is_group) { - // If $string does not contain a colon outside of - // brackets/quotes etc then something's fubar. - - // First check there's a colon at all: - if (strpos($string, ':') === false) { - $this->error = 'Invalid address: ' . $string; - return false; - } - - // Now check it's outside of brackets/quotes: - if (!$this->_splitCheck(explode(':', $string), ':')) { - return false; - } - - // We must have a group at this point, so increase the counter: - $this->num_groups++; - } - - // $string now contains the first full address/group. - // Add to the addresses array. - $this->addresses[] = array( - 'address' => trim($string), - 'group' => $is_group - ); - - // Remove the now stored address from the initial line, the +1 - // is to account for the explode character. - $address = trim(substr($address, strlen($string) + 1)); - - // If the next char is a comma and this was a group, then - // there are more addresses, otherwise, if there are any more - // chars, then there is another address. - if ($is_group && substr($address, 0, 1) == ','){ - $address = trim(substr($address, 1)); - return $address; - - } elseif (strlen($address) > 0) { - return $address; - - } else { - return ''; - } - - // If you got here then something's off - return false; - } - - /** - * Checks for a group at the start of the string. - * - * @access private - * @param string $address The address to check. - * @return boolean Whether or not there is a group at the start of the string. - */ - function _isGroup($address) - { - // First comma not in quotes, angles or escaped: - $parts = explode(',', $address); - $string = $this->_splitCheck($parts, ','); - - // Now we have the first address, we can reliably check for a - // group by searching for a colon that's not escaped or in - // quotes or angle brackets. - if (count($parts = explode(':', $string)) > 1) { - $string2 = $this->_splitCheck($parts, ':'); - return ($string2 !== $string); - } else { - return false; - } - } - - /** - * A common function that will check an exploded string. - * - * @access private - * @param array $parts The exloded string. - * @param string $char The char that was exploded on. - * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. - */ - function _splitCheck($parts, $char) - { - $string = $parts[0]; - - for ($i = 0; $i < count($parts); $i++) { - if ($this->_hasUnclosedQuotes($string) - || $this->_hasUnclosedBrackets($string, '<>') - || $this->_hasUnclosedBrackets($string, '[]') - || $this->_hasUnclosedBrackets($string, '()') - || substr($string, -1) == '\\') { - if (isset($parts[$i + 1])) { - $string = $string . $char . $parts[$i + 1]; - } else { - $this->error = 'Invalid address spec. Unclosed bracket or quotes'; - return false; - } - } else { - $this->index = $i; - break; - } - } - - return $string; - } - - /** - * Checks if a string has unclosed quotes or not. - * - * @access private - * @param string $string The string to check. - * @return boolean True if there are unclosed quotes inside the string, - * false otherwise. - */ - function _hasUnclosedQuotes($string) - { - $string = trim($string); - $iMax = strlen($string); - $in_quote = false; - $i = $slashes = 0; - - for (; $i < $iMax; ++$i) { - switch ($string[$i]) { - case '\\': - ++$slashes; - break; - - case '"': - if ($slashes % 2 == 0) { - $in_quote = !$in_quote; - } - // Fall through to default action below. - - default: - $slashes = 0; - break; - } - } - - return $in_quote; - } - - /** - * Checks if a string has an unclosed brackets or not. IMPORTANT: - * This function handles both angle brackets and square brackets; - * - * @access private - * @param string $string The string to check. - * @param string $chars The characters to check for. - * @return boolean True if there are unclosed brackets inside the string, false otherwise. - */ - function _hasUnclosedBrackets($string, $chars) - { - $num_angle_start = substr_count($string, $chars[0]); - $num_angle_end = substr_count($string, $chars[1]); - - $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); - $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); - - if ($num_angle_start < $num_angle_end) { - $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; - return false; - } else { - return ($num_angle_start > $num_angle_end); - } - } - - /** - * Sub function that is used only by hasUnclosedBrackets(). - * - * @access private - * @param string $string The string to check. - * @param integer &$num The number of occurences. - * @param string $char The character to count. - * @return integer The number of occurences of $char in $string, adjusted for backslashes. - */ - function _hasUnclosedBracketsSub($string, &$num, $char) - { - $parts = explode($char, $string); - for ($i = 0; $i < count($parts); $i++){ - if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) - $num--; - if (isset($parts[$i + 1])) - $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; - } - - return $num; - } - - /** - * Function to begin checking the address. - * - * @access private - * @param string $address The address to validate. - * @return mixed False on failure, or a structured array of address information on success. - */ - function _validateAddress($address) - { - $is_group = false; - $addresses = array(); - - if ($address['group']) { - $is_group = true; - - // Get the group part of the name - $parts = explode(':', $address['address']); - $groupname = $this->_splitCheck($parts, ':'); - $structure = array(); - - // And validate the group part of the name. - if (!$this->_validatePhrase($groupname)){ - $this->error = 'Group name did not validate.'; - return false; - } else { - // Don't include groups if we are not nesting - // them. This avoids returning invalid addresses. - if ($this->nestGroups) { - $structure = new stdClass; - $structure->groupname = $groupname; - } - } - - $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); - } - - // If a group then split on comma and put into an array. - // Otherwise, Just put the whole address in an array. - if ($is_group) { - while (strlen($address['address']) > 0) { - $parts = explode(',', $address['address']); - $addresses[] = $this->_splitCheck($parts, ','); - $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); - } - } else { - $addresses[] = $address['address']; - } - - // Check that $addresses is set, if address like this: - // Groupname:; - // Then errors were appearing. - if (!count($addresses)){ - $this->error = 'Empty group.'; - return false; - } - - // Trim the whitespace from all of the address strings. - array_map('trim', $addresses); - - // Validate each mailbox. - // Format could be one of: name <geezer@domain.com> - // geezer@domain.com - // geezer - // ... or any other format valid by RFC 822. - for ($i = 0; $i < count($addresses); $i++) { - if (!$this->validateMailbox($addresses[$i])) { - if (empty($this->error)) { - $this->error = 'Validation failed for: ' . $addresses[$i]; - } - return false; - } - } - - // Nested format - if ($this->nestGroups) { - if ($is_group) { - $structure->addresses = $addresses; - } else { - $structure = $addresses[0]; - } - - // Flat format - } else { - if ($is_group) { - $structure = array_merge($structure, $addresses); - } else { - $structure = $addresses; - } - } - - return $structure; - } - - /** - * Function to validate a phrase. - * - * @access private - * @param string $phrase The phrase to check. - * @return boolean Success or failure. - */ - function _validatePhrase($phrase) - { - // Splits on one or more Tab or space. - $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); - - $phrase_parts = array(); - while (count($parts) > 0){ - $phrase_parts[] = $this->_splitCheck($parts, ' '); - for ($i = 0; $i < $this->index + 1; $i++) - array_shift($parts); - } - - foreach ($phrase_parts as $part) { - // If quoted string: - if (substr($part, 0, 1) == '"') { - if (!$this->_validateQuotedString($part)) { - return false; - } - continue; - } - - // Otherwise it's an atom: - if (!$this->_validateAtom($part)) return false; - } - - return true; - } - - /** - * Function to validate an atom which from rfc822 is: - * atom = 1*<any CHAR except specials, SPACE and CTLs> - * - * If validation ($this->validate) has been turned off, then - * validateAtom() doesn't actually check anything. This is so that you - * can split a list of addresses up before encoding personal names - * (umlauts, etc.), for example. - * - * @access private - * @param string $atom The string to check. - * @return boolean Success or failure. - */ - function _validateAtom($atom) - { - if (!$this->validate) { - // Validation has been turned off; assume the atom is okay. - return true; - } - - // Check for any char from ASCII 0 - ASCII 127 - if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { - return false; - } - - // Check for specials: - if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { - return false; - } - - // Check for control characters (ASCII 0-31): - if (preg_match('/[\\x00-\\x1F]+/', $atom)) { - return false; - } - - return true; - } - - /** - * Function to validate quoted string, which is: - * quoted-string = <"> *(qtext/quoted-pair) <"> - * - * @access private - * @param string $qstring The string to check - * @return boolean Success or failure. - */ - function _validateQuotedString($qstring) - { - // Leading and trailing " - $qstring = substr($qstring, 1, -1); - - // Perform check, removing quoted characters first. - return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); - } - - /** - * Function to validate a mailbox, which is: - * mailbox = addr-spec ; simple address - * / phrase route-addr ; name and route-addr - * - * @access public - * @param string &$mailbox The string to check. - * @return boolean Success or failure. - */ - function validateMailbox(&$mailbox) - { - // A couple of defaults. - $phrase = ''; - $comment = ''; - $comments = array(); - - // Catch any RFC822 comments and store them separately. - $_mailbox = $mailbox; - while (strlen(trim($_mailbox)) > 0) { - $parts = explode('(', $_mailbox); - $before_comment = $this->_splitCheck($parts, '('); - if ($before_comment != $_mailbox) { - // First char should be a (. - $comment = substr(str_replace($before_comment, '', $_mailbox), 1); - $parts = explode(')', $comment); - $comment = $this->_splitCheck($parts, ')'); - $comments[] = $comment; - - // +1 is for the trailing ) - $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); - } else { - break; - } - } - - foreach ($comments as $comment) { - $mailbox = str_replace("($comment)", '', $mailbox); - } - - $mailbox = trim($mailbox); - - // Check for name + route-addr - if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { - $parts = explode('<', $mailbox); - $name = $this->_splitCheck($parts, '<'); - - $phrase = trim($name); - $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); - - if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { - return false; - } - - // Only got addr-spec - } else { - // First snip angle brackets if present. - if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { - $addr_spec = substr($mailbox, 1, -1); - } else { - $addr_spec = $mailbox; - } - - if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { - return false; - } - } - - // Construct the object that will be returned. - $mbox = new stdClass(); - - // Add the phrase (even if empty) and comments - $mbox->personal = $phrase; - $mbox->comment = isset($comments) ? $comments : array(); - - if (isset($route_addr)) { - $mbox->mailbox = $route_addr['local_part']; - $mbox->host = $route_addr['domain']; - $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; - } else { - $mbox->mailbox = $addr_spec['local_part']; - $mbox->host = $addr_spec['domain']; - } - - $mailbox = $mbox; - return true; - } - - /** - * This function validates a route-addr which is: - * route-addr = "<" [route] addr-spec ">" - * - * Angle brackets have already been removed at the point of - * getting to this function. - * - * @access private - * @param string $route_addr The string to check. - * @return mixed False on failure, or an array containing validated address/route information on success. - */ - function _validateRouteAddr($route_addr) - { - // Check for colon. - if (strpos($route_addr, ':') !== false) { - $parts = explode(':', $route_addr); - $route = $this->_splitCheck($parts, ':'); - } else { - $route = $route_addr; - } - - // If $route is same as $route_addr then the colon was in - // quotes or brackets or, of course, non existent. - if ($route === $route_addr){ - unset($route); - $addr_spec = $route_addr; - if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { - return false; - } - } else { - // Validate route part. - if (($route = $this->_validateRoute($route)) === false) { - return false; - } - - $addr_spec = substr($route_addr, strlen($route . ':')); - - // Validate addr-spec part. - if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { - return false; - } - } - - if (isset($route)) { - $return['adl'] = $route; - } else { - $return['adl'] = ''; - } - - $return = array_merge($return, $addr_spec); - return $return; - } - - /** - * Function to validate a route, which is: - * route = 1#("@" domain) ":" - * - * @access private - * @param string $route The string to check. - * @return mixed False on failure, or the validated $route on success. - */ - function _validateRoute($route) - { - // Split on comma. - $domains = explode(',', trim($route)); - - foreach ($domains as $domain) { - $domain = str_replace('@', '', trim($domain)); - if (!$this->_validateDomain($domain)) return false; - } - - return $route; - } - - /** - * Function to validate a domain, though this is not quite what - * you expect of a strict internet domain. - * - * domain = sub-domain *("." sub-domain) - * - * @access private - * @param string $domain The string to check. - * @return mixed False on failure, or the validated domain on success. - */ - function _validateDomain($domain) - { - // Note the different use of $subdomains and $sub_domains - $subdomains = explode('.', $domain); - - while (count($subdomains) > 0) { - $sub_domains[] = $this->_splitCheck($subdomains, '.'); - for ($i = 0; $i < $this->index + 1; $i++) - array_shift($subdomains); - } - - foreach ($sub_domains as $sub_domain) { - if (!$this->_validateSubdomain(trim($sub_domain))) - return false; - } - - // Managed to get here, so return input. - return $domain; - } - - /** - * Function to validate a subdomain: - * subdomain = domain-ref / domain-literal - * - * @access private - * @param string $subdomain The string to check. - * @return boolean Success or failure. - */ - function _validateSubdomain($subdomain) - { - if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ - if (!$this->_validateDliteral($arr[1])) return false; - } else { - if (!$this->_validateAtom($subdomain)) return false; - } - - // Got here, so return successful. - return true; - } - - /** - * Function to validate a domain literal: - * domain-literal = "[" *(dtext / quoted-pair) "]" - * - * @access private - * @param string $dliteral The string to check. - * @return boolean Success or failure. - */ - function _validateDliteral($dliteral) - { - return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; - } - - /** - * Function to validate an addr-spec. - * - * addr-spec = local-part "@" domain - * - * @access private - * @param string $addr_spec The string to check. - * @return mixed False on failure, or the validated addr-spec on success. - */ - function _validateAddrSpec($addr_spec) - { - $addr_spec = trim($addr_spec); - - // Split on @ sign if there is one. - if (strpos($addr_spec, '@') !== false) { - $parts = explode('@', $addr_spec); - $local_part = $this->_splitCheck($parts, '@'); - $domain = substr($addr_spec, strlen($local_part . '@')); - - // No @ sign so assume the default domain. - } else { - $local_part = $addr_spec; - $domain = $this->default_domain; - } - - if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; - if (($domain = $this->_validateDomain($domain)) === false) return false; - - // Got here so return successful. - return array('local_part' => $local_part, 'domain' => $domain); - } - - /** - * Function to validate the local part of an address: - * local-part = word *("." word) - * - * @access private - * @param string $local_part - * @return mixed False on failure, or the validated local part on success. - */ - function _validateLocalPart($local_part) - { - $parts = explode('.', $local_part); - $words = array(); - - // Split the local_part into words. - while (count($parts) > 0){ - $words[] = $this->_splitCheck($parts, '.'); - for ($i = 0; $i < $this->index + 1; $i++) { - array_shift($parts); - } - } - - // Validate each word. - foreach ($words as $word) { - // If this word contains an unquoted space, it is invalid. (6.2.4) - if (strpos($word, ' ') && $word[0] !== '"') - { - return false; - } - - if ($this->_validatePhrase(trim($word)) === false) return false; - } - - // Managed to get here, so return the input. - return $local_part; - } - - /** - * Returns an approximate count of how many addresses are in the - * given string. This is APPROXIMATE as it only splits based on a - * comma which has no preceding backslash. Could be useful as - * large amounts of addresses will end up producing *large* - * structures when used with parseAddressList(). - * - * @param string $data Addresses to count - * @return int Approximate count - */ - function approximateCount($data) - { - return count(preg_split('/(?<!\\\\),/', $data)); - } - - /** - * This is a email validating function separate to the rest of the - * class. It simply validates whether an email is of the common - * internet form: <user>@<domain>. This can be sufficient for most - * people. Optional stricter mode can be utilised which restricts - * mailbox characters allowed to alphanumeric, full stop, hyphen - * and underscore. - * - * @param string $data Address to check - * @param boolean $strict Optional stricter mode - * @return mixed False if it fails, an indexed array - * username/domain if it matches - */ - function isValidInetAddress($data, $strict = false) - { - $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; - if (preg_match($regex, trim($data), $matches)) { - return array($matches[1], $matches[2]); - } else { - return false; - } - } - -} +<?php +/** + * RFC 822 Email address list validation Utility + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2001-2010, Richard Heyes + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Richard Heyes <richard@phpguru.org> + * @author Chuck Hagenbuch <chuck@horde.org + * @copyright 2001-2010 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: RFC822.php 294749 2010-02-08 08:22:25Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +/** + * RFC 822 Email address list validation Utility + * + * What is it? + * + * This class will take an address string, and parse it into it's consituent + * parts, be that either addresses, groups, or combinations. Nested groups + * are not supported. The structure it returns is pretty straight forward, + * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use + * print_r() to view the structure. + * + * How do I use it? + * + * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;'; + * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true) + * print_r($structure); + * + * @author Richard Heyes <richard@phpguru.org> + * @author Chuck Hagenbuch <chuck@horde.org> + * @version $Revision: 294749 $ + * @license BSD + * @package Mail + */ +class Mail_RFC822 { + + /** + * The address being parsed by the RFC822 object. + * @var string $address + */ + var $address = ''; + + /** + * The default domain to use for unqualified addresses. + * @var string $default_domain + */ + var $default_domain = 'localhost'; + + /** + * Should we return a nested array showing groups, or flatten everything? + * @var boolean $nestGroups + */ + var $nestGroups = true; + + /** + * Whether or not to validate atoms for non-ascii characters. + * @var boolean $validate + */ + var $validate = true; + + /** + * The array of raw addresses built up as we parse. + * @var array $addresses + */ + var $addresses = array(); + + /** + * The final array of parsed address information that we build up. + * @var array $structure + */ + var $structure = array(); + + /** + * The current error message, if any. + * @var string $error + */ + var $error = null; + + /** + * An internal counter/pointer. + * @var integer $index + */ + var $index = null; + + /** + * The number of groups that have been found in the address list. + * @var integer $num_groups + * @access public + */ + var $num_groups = 0; + + /** + * A variable so that we can tell whether or not we're inside a + * Mail_RFC822 object. + * @var boolean $mailRFC822 + */ + var $mailRFC822 = true; + + /** + * A limit after which processing stops + * @var int $limit + */ + var $limit = null; + + /** + * Sets up the object. The address must either be set here or when + * calling parseAddressList(). One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return object Mail_RFC822 A new Mail_RFC822 object. + */ + function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + } + + /** + * Starts the whole process. The address must either be set here + * or when creating the object. One or the other. + * + * @access public + * @param string $address The address(es) to validate. + * @param string $default_domain Default domain/host etc. + * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. + * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. + * + * @return array A structured array of addresses. + */ + function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) + { + if (!isset($this) || !isset($this->mailRFC822)) { + $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); + return $obj->parseAddressList(); + } + + if (isset($address)) $this->address = $address; + if (isset($default_domain)) $this->default_domain = $default_domain; + if (isset($nest_groups)) $this->nestGroups = $nest_groups; + if (isset($validate)) $this->validate = $validate; + if (isset($limit)) $this->limit = $limit; + + $this->structure = array(); + $this->addresses = array(); + $this->error = null; + $this->index = null; + + // Unfold any long lines in $this->address. + $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); + $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); + + while ($this->address = $this->_splitAddresses($this->address)); + + if ($this->address === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + // Validate each address individually. If we encounter an invalid + // address, stop iterating and return an error immediately. + foreach ($this->addresses as $address) { + $valid = $this->_validateAddress($address); + + if ($valid === false || isset($this->error)) { + require_once 'PEAR.php'; + return PEAR::raiseError($this->error); + } + + if (!$this->nestGroups) { + $this->structure = array_merge($this->structure, $valid); + } else { + $this->structure[] = $valid; + } + } + + return $this->structure; + } + + /** + * Splits an address into separate addresses. + * + * @access private + * @param string $address The addresses to split. + * @return boolean Success or failure. + */ + function _splitAddresses($address) + { + if (!empty($this->limit) && count($this->addresses) == $this->limit) { + return ''; + } + + if ($this->_isGroup($address) && !isset($this->error)) { + $split_char = ';'; + $is_group = true; + } elseif (!isset($this->error)) { + $split_char = ','; + $is_group = false; + } elseif (isset($this->error)) { + return false; + } + + // Split the string based on the above ten or so lines. + $parts = explode($split_char, $address); + $string = $this->_splitCheck($parts, $split_char); + + // If a group... + if ($is_group) { + // If $string does not contain a colon outside of + // brackets/quotes etc then something's fubar. + + // First check there's a colon at all: + if (strpos($string, ':') === false) { + $this->error = 'Invalid address: ' . $string; + return false; + } + + // Now check it's outside of brackets/quotes: + if (!$this->_splitCheck(explode(':', $string), ':')) { + return false; + } + + // We must have a group at this point, so increase the counter: + $this->num_groups++; + } + + // $string now contains the first full address/group. + // Add to the addresses array. + $this->addresses[] = array( + 'address' => trim($string), + 'group' => $is_group + ); + + // Remove the now stored address from the initial line, the +1 + // is to account for the explode character. + $address = trim(substr($address, strlen($string) + 1)); + + // If the next char is a comma and this was a group, then + // there are more addresses, otherwise, if there are any more + // chars, then there is another address. + if ($is_group && substr($address, 0, 1) == ','){ + $address = trim(substr($address, 1)); + return $address; + + } elseif (strlen($address) > 0) { + return $address; + + } else { + return ''; + } + + // If you got here then something's off + return false; + } + + /** + * Checks for a group at the start of the string. + * + * @access private + * @param string $address The address to check. + * @return boolean Whether or not there is a group at the start of the string. + */ + function _isGroup($address) + { + // First comma not in quotes, angles or escaped: + $parts = explode(',', $address); + $string = $this->_splitCheck($parts, ','); + + // Now we have the first address, we can reliably check for a + // group by searching for a colon that's not escaped or in + // quotes or angle brackets. + if (count($parts = explode(':', $string)) > 1) { + $string2 = $this->_splitCheck($parts, ':'); + return ($string2 !== $string); + } else { + return false; + } + } + + /** + * A common function that will check an exploded string. + * + * @access private + * @param array $parts The exloded string. + * @param string $char The char that was exploded on. + * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. + */ + function _splitCheck($parts, $char) + { + $string = $parts[0]; + + for ($i = 0; $i < count($parts); $i++) { + if ($this->_hasUnclosedQuotes($string) + || $this->_hasUnclosedBrackets($string, '<>') + || $this->_hasUnclosedBrackets($string, '[]') + || $this->_hasUnclosedBrackets($string, '()') + || substr($string, -1) == '\\') { + if (isset($parts[$i + 1])) { + $string = $string . $char . $parts[$i + 1]; + } else { + $this->error = 'Invalid address spec. Unclosed bracket or quotes'; + return false; + } + } else { + $this->index = $i; + break; + } + } + + return $string; + } + + /** + * Checks if a string has unclosed quotes or not. + * + * @access private + * @param string $string The string to check. + * @return boolean True if there are unclosed quotes inside the string, + * false otherwise. + */ + function _hasUnclosedQuotes($string) + { + $string = trim($string); + $iMax = strlen($string); + $in_quote = false; + $i = $slashes = 0; + + for (; $i < $iMax; ++$i) { + switch ($string[$i]) { + case '\\': + ++$slashes; + break; + + case '"': + if ($slashes % 2 == 0) { + $in_quote = !$in_quote; + } + // Fall through to default action below. + + default: + $slashes = 0; + break; + } + } + + return $in_quote; + } + + /** + * Checks if a string has an unclosed brackets or not. IMPORTANT: + * This function handles both angle brackets and square brackets; + * + * @access private + * @param string $string The string to check. + * @param string $chars The characters to check for. + * @return boolean True if there are unclosed brackets inside the string, false otherwise. + */ + function _hasUnclosedBrackets($string, $chars) + { + $num_angle_start = substr_count($string, $chars[0]); + $num_angle_end = substr_count($string, $chars[1]); + + $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); + $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); + + if ($num_angle_start < $num_angle_end) { + $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; + return false; + } else { + return ($num_angle_start > $num_angle_end); + } + } + + /** + * Sub function that is used only by hasUnclosedBrackets(). + * + * @access private + * @param string $string The string to check. + * @param integer &$num The number of occurences. + * @param string $char The character to count. + * @return integer The number of occurences of $char in $string, adjusted for backslashes. + */ + function _hasUnclosedBracketsSub($string, &$num, $char) + { + $parts = explode($char, $string); + for ($i = 0; $i < count($parts); $i++){ + if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) + $num--; + if (isset($parts[$i + 1])) + $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; + } + + return $num; + } + + /** + * Function to begin checking the address. + * + * @access private + * @param string $address The address to validate. + * @return mixed False on failure, or a structured array of address information on success. + */ + function _validateAddress($address) + { + $is_group = false; + $addresses = array(); + + if ($address['group']) { + $is_group = true; + + // Get the group part of the name + $parts = explode(':', $address['address']); + $groupname = $this->_splitCheck($parts, ':'); + $structure = array(); + + // And validate the group part of the name. + if (!$this->_validatePhrase($groupname)){ + $this->error = 'Group name did not validate.'; + return false; + } else { + // Don't include groups if we are not nesting + // them. This avoids returning invalid addresses. + if ($this->nestGroups) { + $structure = new stdClass; + $structure->groupname = $groupname; + } + } + + $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); + } + + // If a group then split on comma and put into an array. + // Otherwise, Just put the whole address in an array. + if ($is_group) { + while (strlen($address['address']) > 0) { + $parts = explode(',', $address['address']); + $addresses[] = $this->_splitCheck($parts, ','); + $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); + } + } else { + $addresses[] = $address['address']; + } + + // Check that $addresses is set, if address like this: + // Groupname:; + // Then errors were appearing. + if (!count($addresses)){ + $this->error = 'Empty group.'; + return false; + } + + // Trim the whitespace from all of the address strings. + array_map('trim', $addresses); + + // Validate each mailbox. + // Format could be one of: name <geezer@domain.com> + // geezer@domain.com + // geezer + // ... or any other format valid by RFC 822. + for ($i = 0; $i < count($addresses); $i++) { + if (!$this->validateMailbox($addresses[$i])) { + if (empty($this->error)) { + $this->error = 'Validation failed for: ' . $addresses[$i]; + } + return false; + } + } + + // Nested format + if ($this->nestGroups) { + if ($is_group) { + $structure->addresses = $addresses; + } else { + $structure = $addresses[0]; + } + + // Flat format + } else { + if ($is_group) { + $structure = array_merge($structure, $addresses); + } else { + $structure = $addresses; + } + } + + return $structure; + } + + /** + * Function to validate a phrase. + * + * @access private + * @param string $phrase The phrase to check. + * @return boolean Success or failure. + */ + function _validatePhrase($phrase) + { + // Splits on one or more Tab or space. + $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); + + $phrase_parts = array(); + while (count($parts) > 0){ + $phrase_parts[] = $this->_splitCheck($parts, ' '); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($parts); + } + + foreach ($phrase_parts as $part) { + // If quoted string: + if (substr($part, 0, 1) == '"') { + if (!$this->_validateQuotedString($part)) { + return false; + } + continue; + } + + // Otherwise it's an atom: + if (!$this->_validateAtom($part)) return false; + } + + return true; + } + + /** + * Function to validate an atom which from rfc822 is: + * atom = 1*<any CHAR except specials, SPACE and CTLs> + * + * If validation ($this->validate) has been turned off, then + * validateAtom() doesn't actually check anything. This is so that you + * can split a list of addresses up before encoding personal names + * (umlauts, etc.), for example. + * + * @access private + * @param string $atom The string to check. + * @return boolean Success or failure. + */ + function _validateAtom($atom) + { + if (!$this->validate) { + // Validation has been turned off; assume the atom is okay. + return true; + } + + // Check for any char from ASCII 0 - ASCII 127 + if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { + return false; + } + + // Check for specials: + if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { + return false; + } + + // Check for control characters (ASCII 0-31): + if (preg_match('/[\\x00-\\x1F]+/', $atom)) { + return false; + } + + return true; + } + + /** + * Function to validate quoted string, which is: + * quoted-string = <"> *(qtext/quoted-pair) <"> + * + * @access private + * @param string $qstring The string to check + * @return boolean Success or failure. + */ + function _validateQuotedString($qstring) + { + // Leading and trailing " + $qstring = substr($qstring, 1, -1); + + // Perform check, removing quoted characters first. + return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); + } + + /** + * Function to validate a mailbox, which is: + * mailbox = addr-spec ; simple address + * / phrase route-addr ; name and route-addr + * + * @access public + * @param string &$mailbox The string to check. + * @return boolean Success or failure. + */ + function validateMailbox(&$mailbox) + { + // A couple of defaults. + $phrase = ''; + $comment = ''; + $comments = array(); + + // Catch any RFC822 comments and store them separately. + $_mailbox = $mailbox; + while (strlen(trim($_mailbox)) > 0) { + $parts = explode('(', $_mailbox); + $before_comment = $this->_splitCheck($parts, '('); + if ($before_comment != $_mailbox) { + // First char should be a (. + $comment = substr(str_replace($before_comment, '', $_mailbox), 1); + $parts = explode(')', $comment); + $comment = $this->_splitCheck($parts, ')'); + $comments[] = $comment; + + // +2 is for the brackets + $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); + } else { + break; + } + } + + foreach ($comments as $comment) { + $mailbox = str_replace("($comment)", '', $mailbox); + } + + $mailbox = trim($mailbox); + + // Check for name + route-addr + if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { + $parts = explode('<', $mailbox); + $name = $this->_splitCheck($parts, '<'); + + $phrase = trim($name); + $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); + + if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { + return false; + } + + // Only got addr-spec + } else { + // First snip angle brackets if present. + if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { + $addr_spec = substr($mailbox, 1, -1); + } else { + $addr_spec = $mailbox; + } + + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + // Construct the object that will be returned. + $mbox = new stdClass(); + + // Add the phrase (even if empty) and comments + $mbox->personal = $phrase; + $mbox->comment = isset($comments) ? $comments : array(); + + if (isset($route_addr)) { + $mbox->mailbox = $route_addr['local_part']; + $mbox->host = $route_addr['domain']; + $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; + } else { + $mbox->mailbox = $addr_spec['local_part']; + $mbox->host = $addr_spec['domain']; + } + + $mailbox = $mbox; + return true; + } + + /** + * This function validates a route-addr which is: + * route-addr = "<" [route] addr-spec ">" + * + * Angle brackets have already been removed at the point of + * getting to this function. + * + * @access private + * @param string $route_addr The string to check. + * @return mixed False on failure, or an array containing validated address/route information on success. + */ + function _validateRouteAddr($route_addr) + { + // Check for colon. + if (strpos($route_addr, ':') !== false) { + $parts = explode(':', $route_addr); + $route = $this->_splitCheck($parts, ':'); + } else { + $route = $route_addr; + } + + // If $route is same as $route_addr then the colon was in + // quotes or brackets or, of course, non existent. + if ($route === $route_addr){ + unset($route); + $addr_spec = $route_addr; + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } else { + // Validate route part. + if (($route = $this->_validateRoute($route)) === false) { + return false; + } + + $addr_spec = substr($route_addr, strlen($route . ':')); + + // Validate addr-spec part. + if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { + return false; + } + } + + if (isset($route)) { + $return['adl'] = $route; + } else { + $return['adl'] = ''; + } + + $return = array_merge($return, $addr_spec); + return $return; + } + + /** + * Function to validate a route, which is: + * route = 1#("@" domain) ":" + * + * @access private + * @param string $route The string to check. + * @return mixed False on failure, or the validated $route on success. + */ + function _validateRoute($route) + { + // Split on comma. + $domains = explode(',', trim($route)); + + foreach ($domains as $domain) { + $domain = str_replace('@', '', trim($domain)); + if (!$this->_validateDomain($domain)) return false; + } + + return $route; + } + + /** + * Function to validate a domain, though this is not quite what + * you expect of a strict internet domain. + * + * domain = sub-domain *("." sub-domain) + * + * @access private + * @param string $domain The string to check. + * @return mixed False on failure, or the validated domain on success. + */ + function _validateDomain($domain) + { + // Note the different use of $subdomains and $sub_domains + $subdomains = explode('.', $domain); + + while (count($subdomains) > 0) { + $sub_domains[] = $this->_splitCheck($subdomains, '.'); + for ($i = 0; $i < $this->index + 1; $i++) + array_shift($subdomains); + } + + foreach ($sub_domains as $sub_domain) { + if (!$this->_validateSubdomain(trim($sub_domain))) + return false; + } + + // Managed to get here, so return input. + return $domain; + } + + /** + * Function to validate a subdomain: + * subdomain = domain-ref / domain-literal + * + * @access private + * @param string $subdomain The string to check. + * @return boolean Success or failure. + */ + function _validateSubdomain($subdomain) + { + if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ + if (!$this->_validateDliteral($arr[1])) return false; + } else { + if (!$this->_validateAtom($subdomain)) return false; + } + + // Got here, so return successful. + return true; + } + + /** + * Function to validate a domain literal: + * domain-literal = "[" *(dtext / quoted-pair) "]" + * + * @access private + * @param string $dliteral The string to check. + * @return boolean Success or failure. + */ + function _validateDliteral($dliteral) + { + return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\'; + } + + /** + * Function to validate an addr-spec. + * + * addr-spec = local-part "@" domain + * + * @access private + * @param string $addr_spec The string to check. + * @return mixed False on failure, or the validated addr-spec on success. + */ + function _validateAddrSpec($addr_spec) + { + $addr_spec = trim($addr_spec); + + // Split on @ sign if there is one. + if (strpos($addr_spec, '@') !== false) { + $parts = explode('@', $addr_spec); + $local_part = $this->_splitCheck($parts, '@'); + $domain = substr($addr_spec, strlen($local_part . '@')); + + // No @ sign so assume the default domain. + } else { + $local_part = $addr_spec; + $domain = $this->default_domain; + } + + if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; + if (($domain = $this->_validateDomain($domain)) === false) return false; + + // Got here so return successful. + return array('local_part' => $local_part, 'domain' => $domain); + } + + /** + * Function to validate the local part of an address: + * local-part = word *("." word) + * + * @access private + * @param string $local_part + * @return mixed False on failure, or the validated local part on success. + */ + function _validateLocalPart($local_part) + { + $parts = explode('.', $local_part); + $words = array(); + + // Split the local_part into words. + while (count($parts) > 0){ + $words[] = $this->_splitCheck($parts, '.'); + for ($i = 0; $i < $this->index + 1; $i++) { + array_shift($parts); + } + } + + // Validate each word. + foreach ($words as $word) { + // If this word contains an unquoted space, it is invalid. (6.2.4) + if (strpos($word, ' ') && $word[0] !== '"') + { + return false; + } + + if ($this->_validatePhrase(trim($word)) === false) return false; + } + + // Managed to get here, so return the input. + return $local_part; + } + + /** + * Returns an approximate count of how many addresses are in the + * given string. This is APPROXIMATE as it only splits based on a + * comma which has no preceding backslash. Could be useful as + * large amounts of addresses will end up producing *large* + * structures when used with parseAddressList(). + * + * @param string $data Addresses to count + * @return int Approximate count + */ + function approximateCount($data) + { + return count(preg_split('/(?<!\\\\),/', $data)); + } + + /** + * This is a email validating function separate to the rest of the + * class. It simply validates whether an email is of the common + * internet form: <user>@<domain>. This can be sufficient for most + * people. Optional stricter mode can be utilised which restricts + * mailbox characters allowed to alphanumeric, full stop, hyphen + * and underscore. + * + * @param string $data Address to check + * @param boolean $strict Optional stricter mode + * @return mixed False if it fails, an indexed array + * username/domain if it matches + */ + function isValidInetAddress($data, $strict = false) + { + $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; + if (preg_match($regex, trim($data), $matches)) { + return array($matches[1], $matches[2]); + } else { + return false; + } + } + +} diff --git a/include/pear/Mail/mail.php b/include/pear/Mail/mail.php index b48d2697fa9acbad7b835ef8989dcb7fd9291297..a8b4b5dbeef6c0b194fa9a2036af40c2feb5869a 100644 --- a/include/pear/Mail/mail.php +++ b/include/pear/Mail/mail.php @@ -1,143 +1,168 @@ -<?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Author: Chuck Hagenbuch <chuck@horde.org> | -// +----------------------------------------------------------------------+ -// -// $Id: mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ - -/** - * internal PHP-mail() implementation of the PEAR Mail:: interface. - * @package Mail - * @version $Revision: 1.20 $ - */ -class Mail_mail extends Mail { - - /** - * Any arguments to pass to the mail() function. - * @var string - */ - var $_params = ''; - - /** - * Constructor. - * - * Instantiates a new Mail_mail:: object based on the parameters - * passed in. - * - * @param array $params Extra arguments for the mail() function. - */ - function Mail_mail($params = null) - { - // The other mail implementations accept parameters as arrays. - // In the interest of being consistent, explode an array into - // a string of parameter arguments. - if (is_array($params)) { - $this->_params = join(' ', $params); - } else { - $this->_params = $params; - } - - /* Because the mail() function may pass headers as command - * line arguments, we can't guarantee the use of the standard - * "\r\n" separator. Instead, we use the system's native line - * separator. */ - if (defined('PHP_EOL')) { - $this->sep = PHP_EOL; - } else { - $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; - } - } - - /** - * Implements Mail_mail::send() function using php's built-in mail() - * command. - * - * @param mixed $recipients Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. This may contain recipients not - * specified in the headers, for Bcc:, resending - * messages, etc. - * - * @param array $headers The array of headers to send with the mail, in an - * associative array, where the array key is the - * header name (ie, 'Subject'), and the array value - * is the header value (ie, 'test'). The header - * produced from those values would be 'Subject: - * test'. - * - * @param string $body The full text of the message body, including any - * Mime parts, etc. - * - * @return mixed Returns true on success, or a PEAR_Error - * containing a descriptive error message on - * failure. - * - * @access public - */ - function send($recipients, $headers, $body) - { - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } - - $result = $this->_sanitizeHeaders($headers); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - // If we're passed an array of recipients, implode it. - if (is_array($recipients)) { - $recipients = implode(', ', $recipients); - } - - // Get the Subject out of the headers array so that we can - // pass it as a seperate argument to mail(). - $subject = ''; - if (isset($headers['Subject'])) { - $subject = $headers['Subject']; - unset($headers['Subject']); - } - - // Also remove the To: header. The mail() function will add its own - // To: header based on the contents of $recipients. - unset($headers['To']); - - // Flatten the headers out. - $headerElements = $this->prepareHeaders($headers); - if (is_a($headerElements, 'PEAR_Error')) { - return $headerElements; - } - list(, $text_headers) = $headerElements; - - // We only use mail()'s optional fifth parameter if the additional - // parameters have been provided and we're not running in safe mode. - if (empty($this->_params) || ini_get('safe_mode')) { - $result = mail($recipients, $subject, $body, $text_headers); - } else { - $result = mail($recipients, $subject, $body, $text_headers, - $this->_params); - } - - // If the mail() function returned failure, we need to create a - // PEAR_Error object and return it instead of the boolean result. - if ($result === false) { - $result = PEAR::raiseError('mail() returned failure'); - } - - return $result; - } - -} +<?php +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: mail.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * @package Mail + * @version $Revision: 294747 $ + */ +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 index 2286920d76625b5fa1f9eb9537cd22bb4d8c5019..4522e20cd2eab289a3be6b39126c3aafc81e18a3 100644 --- a/include/pear/Mail/mime.php +++ b/include/pear/Mail/mime.php @@ -45,9 +45,10 @@ * @author Tomas V.V. Cox <cox@idecnet.com> * @author Cipriano Groenendal <cipri@php.net> * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> * @copyright 2003-2006 PEAR <pear-group@php.net> * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version CVS: $Id: mime.php,v 1.81 2007/06/21 19:08:28 cipri Exp $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Mail_mime * * This class is based on HTML Mime Mail class from @@ -109,22 +110,6 @@ class Mail_mime */ 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 * @@ -142,66 +127,97 @@ class Mail_mime var $_parts = array(); /** - * Build parameters + * Headers for the mail * * @var array * @access private */ - var $_build_params = array(); + var $_headers = array(); /** - * Headers for the mail + * Build parameters * * @var array * @access private */ - var $_headers = array(); + var $_build_params = array( + // What encoding to use for the headers + // Options: quoted-printable or base64 + 'head_encoding' => 'quoted-printable', + // What encoding to use for plain text + // Options: 7bit, 8bit, base64, or quoted-printable + 'text_encoding' => 'quoted-printable', + // What encoding to use for html + // Options: 7bit, 8bit, base64, or quoted-printable + 'html_encoding' => 'quoted-printable', + // The character set to use for html + 'html_charset' => 'ISO-8859-1', + // The character set to use for text + 'text_charset' => 'ISO-8859-1', + // The character set to use for headers + 'head_charset' => 'ISO-8859-1', + // End-of-line sequence + 'eol' => "\r\n", + // Delay attachment files IO until building the message + 'delay_file_io' => false + ); /** - * End Of Line sequence (for serialize) + * Constructor function * - * @var string - * @access private + * @param mixed $params Build parameters that change the way the email + * is built. Should be an associative array. + * See $_build_params. + * + * @return void + * @access public */ - var $_eol; + function Mail_mime($params = array()) + { + // Backward-compatible EOL setting + if (is_string($params)) { + $this->_build_params['eol'] = $params; + } else if (defined('MAIL_MIME_CRLF') && !isset($params['eol'])) { + $this->_build_params['eol'] = MAIL_MIME_CRLF; + } + // Update build parameters + if (!empty($params) && is_array($params)) { + while (list($key, $value) = each($params)) { + $this->_build_params[$key] = $value; + } + } + } /** - * Constructor function. + * Set build parameter value * - * @param string $crlf what type of linebreak to use. - * Defaults to "\r\n" + * @param string $name Parameter name + * @param string $value Parameter value * * @return void - * * @access public + * @since 1.6.0 */ - function Mail_mime($crlf = "\r\n") + function setParam($name, $value) { - $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' - ); + $this->_build_params[$name] = $value; } /** - * wakeup function called by unserialize. It re-sets the EOL constant + * Get build parameter value * - * @access private - * @return void + * @param string $name Parameter name + * + * @return mixed Parameter value + * @access public + * @since 1.6.0 */ - function __wakeup() + function getParam($name) { - $this->_setEOL($this->_eol); + return isset($this->_build_params[$name]) ? $this->_build_params[$name] : null; } - /** * 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 @@ -209,14 +225,14 @@ class Mail_mime * html should show. * * @param string $data Either a string or - * the file name with the contents + * 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) + * 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 + * the existing body, else the old body is + * overwritten * - * @return mixed true on success or PEAR_Error object + * @return mixed True on success or PEAR_Error object * @access public */ function setTXTBody($data, $isfile = false, $append = false) @@ -241,15 +257,27 @@ class Mail_mime return true; } + /** + * Get message text body + * + * @return string Text body + * @access public + * @since 1.6.0 + */ + function getTXTBody() + { + return $this->_txtbody; + } + /** * 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) + * @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 + * @return bool True on success * @access public */ function setHTMLBody($data, $isfile = false) @@ -267,38 +295,69 @@ class Mail_mime return true; } + /** + * Get message HTML body + * + * @return string HTML body + * @access public + * @since 1.6.0 + */ + function getHTMLBody() + { + return $this->_htmlbody; + } + /** * 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 + * @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 + * @param string $content_id Desired Content-ID of MIME part + * Defaults to generated unique ID * - * @return bool true on success + * @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); + function addHTMLImage($file, + $c_type='application/octet-stream', + $name = '', + $isfile = true, + $content_id = null + ) { + $bodyfile = null; + + if ($isfile) { + // Don't load file into memory + if ($this->_build_params['delay_file_io']) { + $filedata = null; + $bodyfile = $file; + } else { + if (PEAR::isError($filedata = $this->_file2str($file))) { + return $filedata; + } + } + $filename = ($name ? $name : $file); } else { + $filedata = $file; $filename = $name; } - if (PEAR::isError($filedata)) { - return $filedata; + + if (!$content_id) { + $content_id = md5(uniqid(time())); } + $this->_html_images[] = array( - 'body' => $filedata, - 'name' => $filename, - 'c_type' => $c_type, - 'cid' => md5(uniqid(time())) - ); + 'body' => $filedata, + 'body_file' => $bodyfile, + 'name' => $filename, + 'c_type' => $c_type, + 'cid' => $content_id + ); + return true; } @@ -306,99 +365,128 @@ class Mail_mime * Adds a file to the list of attachments. * * @param string $file The file name of the file to attach - * OR the file contents itself + * 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. + * 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. + * Defaults to attachment. + * Possible values: attachment, inline. + * @param string $charset The character set of attachment's content. * @param string $language The language of the attachment * @param string $location The RFC 2557.4 location of the attachment + * @param string $n_encoding Encoding of the attachment's name in Content-Type + * By default filenames are encoded using RFC2231 method + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * @param string $f_encoding Encoding of the attachment's filename + * in Content-Disposition header. + * @param string $description Content-Description header + * @param string $h_charset The character set of the headers e.g. filename + * If not specified, $charset will be used + * @param array $add_headers Additional part headers. Array keys can be in form + * of <header_name>:<parameter_name> * - * @return mixed true on success or PEAR_Error object + * @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) { + $c_type = 'application/octet-stream', + $name = '', + $isfile = true, + $encoding = 'base64', + $disposition = 'attachment', + $charset = '', + $language = '', + $location = '', + $n_encoding = null, + $f_encoding = null, + $description = '', + $h_charset = null, + $add_headers = array() + ) { + $bodyfile = null; + + if ($isfile) { + // Don't load file into memory + if ($this->_build_params['delay_file_io']) { + $filedata = null; + $bodyfile = $file; + } else { + if (PEAR::isError($filedata = $this->_file2str($file))) { + return $filedata; + } + } // Force the name the user supplied, otherwise use $file - $filename = (strlen($name)) ? $name : $file; + $filename = ($name ? $name : $file); } else { + $filedata = $file; $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; - } + $filename = $this->_basename($filename); $this->_parts[] = array( - 'body' => $filedata, - 'name' => $filename, - 'c_type' => $c_type, - 'encoding' => $encoding, - 'charset' => $charset, - 'language' => $language, - 'location' => $location, - 'disposition' => $disposition - ); + 'body' => $filedata, + 'body_file' => $bodyfile, + 'name' => $filename, + 'c_type' => $c_type, + 'charset' => $charset, + 'encoding' => $encoding, + 'language' => $language, + 'location' => $location, + 'disposition' => $disposition, + 'description' => $description, + 'add_headers' => $add_headers, + 'name_encoding' => $n_encoding, + 'filename_encoding' => $f_encoding, + 'headers_charset' => $h_charset, + ); + return true; } /** * Get the contents of the given file name as string * - * @param string $file_name path of file to process + * @param string $file_name Path of file to process * - * @return string contents of $file_name + * @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); + // Check state of file and raise an error properly + if (!file_exists($file_name)) { + $err = PEAR::raiseError('File not found: ' . $file_name); return $err; } - if (!$fd = fopen($file_name, 'rb')) { - $err = PEAR::raiseError('Could not open ' . $file_name); + if (!is_file($file_name)) { + $err = PEAR::raiseError('Not a regular file: ' . $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); - } + if (!is_readable($file_name)) { + $err = PEAR::raiseError('File is not readable: ' . $file_name); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + $cont = file_get_contents($file_name); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); } - fclose($fd); + return $cont; } @@ -407,10 +495,10 @@ class Mail_mime * 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. + * null if a new object is to be created. * @param string $text The text to add. * - * @return object The text mimePart object + * @return object The text mimePart object * @access private */ function &_addTextPart(&$obj, $text) @@ -418,6 +506,8 @@ class Mail_mime $params['content_type'] = 'text/plain'; $params['encoding'] = $this->_build_params['text_encoding']; $params['charset'] = $this->_build_params['text_charset']; + $params['eol'] = $this->_build_params['eol']; + if (is_object($obj)) { $ret = $obj->addSubpart($text, $params); return $ret; @@ -432,9 +522,9 @@ class Mail_mime * 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. + * null if a new object is to be created. * - * @return object The html mimePart object + * @return object The html mimePart object * @access private */ function &_addHtmlPart(&$obj) @@ -442,6 +532,8 @@ class Mail_mime $params['content_type'] = 'text/html'; $params['encoding'] = $this->_build_params['html_encoding']; $params['charset'] = $this->_build_params['html_charset']; + $params['eol'] = $this->_build_params['eol']; + if (is_object($obj)) { $ret = $obj->addSubpart($this->_htmlbody, $params); return $ret; @@ -463,8 +555,9 @@ class Mail_mime { $params = array(); $params['content_type'] = 'multipart/mixed'; - - //Create empty multipart/mixed Mail_mimePart object to return + $params['eol'] = $this->_build_params['eol']; + + // Create empty multipart/mixed Mail_mimePart object to return $ret = new Mail_mimePart('', $params); return $ret; } @@ -475,14 +568,16 @@ class Mail_mime * the build process. * * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created. + * null if a new object is to be created. * - * @return object The multipart/mixed mimePart object + * @return object The multipart/mixed mimePart object * @access private */ function &_addAlternativePart(&$obj) { $params['content_type'] = 'multipart/alternative'; + $params['eol'] = $this->_build_params['eol']; + if (is_object($obj)) { return $obj->addSubpart('', $params); } else { @@ -497,14 +592,16 @@ class Mail_mime * the build process. * * @param mixed &$obj The object to add the part to, or - * null if a new object is to be created + * null if a new object is to be created * - * @return object The multipart/mixed mimePart object + * @return object The multipart/mixed mimePart object * @access private */ function &_addRelatedPart(&$obj) { $params['content_type'] = 'multipart/related'; + $params['eol'] = $this->_build_params['eol']; + if (is_object($obj)) { return $obj->addSubpart('', $params); } else { @@ -520,7 +617,7 @@ class Mail_mime * @param object &$obj The mimePart to add the image to * @param array $value The image information * - * @return object The image mimePart object + * @return object The image mimePart object * @access private */ function &_addHtmlImagePart(&$obj, $value) @@ -528,12 +625,20 @@ class Mail_mime $params['content_type'] = $value['c_type']; $params['encoding'] = 'base64'; $params['disposition'] = 'inline'; - $params['dfilename'] = $value['name']; + $params['filename'] = $value['name']; $params['cid'] = $value['cid']; - + $params['body_file'] = $value['body_file']; + $params['eol'] = $this->_build_params['eol']; + + if (!empty($value['name_encoding'])) { + $params['name_encoding'] = $value['name_encoding']; + } + if (!empty($value['filename_encoding'])) { + $params['filename_encoding'] = $value['filename_encoding']; + } + $ret = $obj->addSubpart($value['body'], $params); return $ret; - } /** @@ -543,25 +648,46 @@ class Mail_mime * @param object &$obj The mimePart to add the image to * @param array $value The attachment information * - * @return object The image mimePart object + * @return object The image mimePart object * @access private */ function &_addAttachmentPart(&$obj, $value) { - $params['dfilename'] = $value['name']; - $params['encoding'] = $value['encoding']; - if ($value['charset']) { + $params['eol'] = $this->_build_params['eol']; + $params['filename'] = $value['name']; + $params['encoding'] = $value['encoding']; + $params['content_type'] = $value['c_type']; + $params['body_file'] = $value['body_file']; + $params['disposition'] = isset($value['disposition']) ? + $value['disposition'] : 'attachment'; + + // content charset + if (!empty($value['charset'])) { $params['charset'] = $value['charset']; } - if ($value['language']) { + // headers charset (filename, description) + if (!empty($value['headers_charset'])) { + $params['headers_charset'] = $value['headers_charset']; + } + if (!empty($value['language'])) { $params['language'] = $value['language']; } - if ($value['location']) { + if (!empty($value['location'])) { $params['location'] = $value['location']; } - $params['content_type'] = $value['c_type']; - $params['disposition'] = isset($value['disposition']) ? - $value['disposition'] : 'attachment'; + if (!empty($value['name_encoding'])) { + $params['name_encoding'] = $value['name_encoding']; + } + if (!empty($value['filename_encoding'])) { + $params['filename_encoding'] = $value['filename_encoding']; + } + if (!empty($value['description'])) { + $params['description'] = $value['description']; + } + if (is_array($value['add_headers'])) { + $params['headers'] = $value['add_headers']; + } + $ret = $obj->addSubpart($value['body'], $params); return $ret; } @@ -571,85 +697,178 @@ class Mail_mime * 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! + * using the $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. + * @param string $separation The separation between these two parts. + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $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 mixed The complete e-mail or PEAR error object * @access public */ - function getMessage( - $separation = null, - $build_params = null, - $xtra_headers = null, - $overwrite = false - ) - { + function getMessage($separation = null, $params = null, $headers = null, + $overwrite = false + ) { if ($separation === null) { - $separation = MAIL_MIME_CRLF; + $separation = $this->_build_params['eol']; } - $body = $this->get($build_params); - $head = $this->txtHeaders($xtra_headers, $overwrite); + + $body = $this->get($params); + + if (PEAR::isError($body)) { + return $body; + } + + $head = $this->txtHeaders($headers, $overwrite); $mail = $head . $separation . $body; return $mail; } + /** + * Returns the complete e-mail body, ready to send using an alternative + * mail delivery method. + * + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * + * @return mixed The e-mail body or PEAR error object + * @access public + * @since 1.6.0 + */ + function getMessageBody($params = null) + { + return $this->get($params, null, true); + } + + /** + * Writes (appends) the complete e-mail into file. + * + * @param string $filename Output file location + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $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 mixed True or PEAR error object + * @access public + * @since 1.6.0 + */ + function saveMessage($filename, $params = null, $headers = null, $overwrite = false) + { + // Check state of file and raise an error properly + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writable: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Write message headers into file (skipping Content-* headers) + $head = $this->txtHeaders($headers, $overwrite, true); + if (fwrite($fh, $head) === false) { + $err = PEAR::raiseError('Error writing to file: ' . $filename); + return $err; + } + + fclose($fh); + + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + // Write the rest of the message into file + $res = $this->get($params, $filename); + + return $res ? $res : true; + } + + /** + * Writes (appends) the complete e-mail body into file. + * + * @param string $filename Output file location + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * + * @return mixed True or PEAR error object + * @access public + * @since 1.6.0 + */ + function saveMessageBody($filename, $params = null) + { + // Check state of file and raise an error properly + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writable: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Write the rest of the message into file + $res = $this->get($params, $filename, true); + + return $res ? $res : true; + } /** * 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 + * @param array $params Build parameters that change the way the email + * is built. Should be associative. See $_build_params. + * @param resource $filename Output file where to save the message instead of + * returning it + * @param boolean $skip_head True if you want to return/save only the message + * without headers + * + * @return mixed The MIME message content string, null or PEAR error object * @access public */ - function &get($build_params = null) + function &get($params = null, $filename = null, $skip_head = false) { - if (isset($build_params)) { - while (list($key, $value) = each($build_params)) { + if (isset($params)) { + while (list($key, $value) = each($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 (isset($this->_headers['From'])) { + // Bug #11381: Illegal characters in domain ID + if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->_headers['From'], $matches)) { + $domainID = $matches[1]; + } else { + $domainID = '@localhost'; + } + foreach ($this->_html_images as $i => $img) { + $cid = $this->_html_images[$i]['cid']; + if (!preg_match('#'.preg_quote($domainID).'$#', $cid)) { + $this->_html_images[$i]['cid'] = $cid . $domainID; + } } } - - if (count($this->_html_images) AND isset($this->_htmlbody)) { + + if (count($this->_html_images) && isset($this->_htmlbody)) { foreach ($this->_html_images as $key => $value) { $regex = array(); $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . @@ -659,33 +878,35 @@ class Mail_mime $rep = array(); $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3'; - $rep[] = 'url(\1cid:' . $value['cid'] . '\2)'; + $rep[] = 'url(\1cid:' . $value['cid'] . '\1)'; $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody); - $this->_html_images[$key]['name'] = - basename($this->_html_images[$key]['name']); + $this->_html_images[$key]['name'] + = $this->_basename($this->_html_images[$key]['name']); } } + $this->_checkParams(); + $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; + $text = (!$html && strlen($this->_txtbody)) ? true : false; switch (true) { - case $text AND !$attachments: + case $text && !$attachments: $message =& $this->_addTextPart($null, $this->_txtbody); break; - case !$text AND !$html AND $attachments: + case !$text && !$html && $attachments: $message =& $this->_addMixedPart(); for ($i = 0; $i < count($this->_parts); $i++) { $this->_addAttachmentPart($message, $this->_parts[$i]); } break; - case $text AND $attachments: + case $text && $attachments: $message =& $this->_addMixedPart(); $this->_addTextPart($message, $this->_txtbody); for ($i = 0; $i < count($this->_parts); $i++) { @@ -693,7 +914,7 @@ class Mail_mime } break; - case $html AND !$attachments AND !$html_images: + case $html && !$attachments && !$html_images: if (isset($this->_txtbody)) { $message =& $this->_addAlternativePart($null); $this->_addTextPart($message, $this->_txtbody); @@ -703,7 +924,38 @@ class Mail_mime } break; - case $html AND !$attachments AND $html_images: + case $html && !$attachments && $html_images: + // * Content-Type: multipart/alternative; + // * text + // * Content-Type: multipart/related; + // * html + // * image... + if (isset($this->_txtbody)) { + $message =& $this->_addAlternativePart($null); + $this->_addTextPart($message, $this->_txtbody); + + $ht =& $this->_addRelatedPart($message); + $this->_addHtmlPart($ht); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($ht, $this->_html_images[$i]); + } + } else { + // * Content-Type: multipart/related; + // * html + // * image... + $message =& $this->_addRelatedPart($null); + $this->_addHtmlPart($message); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($message, $this->_html_images[$i]); + } + } + /* + // #13444, #9725: the code below was a non-RFC compliant hack + // * Content-Type: multipart/related; + // * Content-Type: multipart/alternative; + // * text + // * html + // * image... $message =& $this->_addRelatedPart($null); if (isset($this->_txtbody)) { $alt =& $this->_addAlternativePart($message); @@ -715,9 +967,10 @@ class Mail_mime for ($i = 0; $i < count($this->_html_images); $i++) { $this->_addHtmlImagePart($message, $this->_html_images[$i]); } + */ break; - case $html AND $attachments AND !$html_images: + case $html && $attachments && !$html_images: $message =& $this->_addMixedPart(); if (isset($this->_txtbody)) { $alt =& $this->_addAlternativePart($message); @@ -731,7 +984,7 @@ class Mail_mime } break; - case $html AND $attachments AND $html_images: + case $html && $attachments && $html_images: $message =& $this->_addMixedPart(); if (isset($this->_txtbody)) { $alt =& $this->_addAlternativePart($message); @@ -751,17 +1004,36 @@ class Mail_mime } - if (isset($message)) { - $output = $message->encode(); - - $this->_headers = array_merge($this->_headers, - $output['headers']); - $body = $output['body']; - return $body; + if (!isset($message)) { + $ret = null; + return $ret; + } + // Use saved boundary + if (!empty($this->_build_params['boundary'])) { + $boundary = $this->_build_params['boundary']; } else { - $ret = false; + $boundary = null; + } + + // Write output to file + if ($filename) { + // Append mimePart message headers and body into file + $headers = $message->encodeToFile($filename, $boundary, $skip_head); + if (PEAR::isError($headers)) { + return $headers; + } + $this->_headers = array_merge($this->_headers, $headers); + $ret = null; return $ret; + } else { + $output = $message->encode($boundary, $skip_head); + if (PEAR::isError($output)) { + return $output; + } + $this->_headers = array_merge($this->_headers, $output['headers']); + $body = $output['body']; + return $body; } } @@ -770,28 +1042,49 @@ class Mail_mime * (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 array $xtra_headers Assoc array with any extra headers (optional) + * (Don't set Content-Type for multipart messages here!) * @param bool $overwrite Overwrite already existing headers. + * @param bool $skip_content Don't return content headers: Content-Type, + * Content-Disposition and Content-Transfer-Encoding * - * @return array Assoc array with the mime headers + * @return array Assoc array with the mime headers * @access public */ - function &headers($xtra_headers = null, $overwrite = false) + function &headers($xtra_headers = null, $overwrite = false, $skip_content = false) { - // Content-Type header should already be present, - // So just add mime version header + // Add mime version header $headers['MIME-Version'] = '1.0'; - if (isset($xtra_headers)) { + + // Content-Type and Content-Transfer-Encoding headers should already + // be present if get() was called, but we'll re-set them to make sure + // we got them when called before get() or something in the message + // has been changed after get() [#14780] + if (!$skip_content) { + $headers += $this->_contentHeaders(); + } + + if (!empty($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); + $headers = $this->_headers; + + if ($skip_content) { + unset($headers['Content-Type']); + unset($headers['Content-Transfer-Encoding']); + unset($headers['Content-Disposition']); + } else if (!empty($this->_build_params['ctype'])) { + $headers['Content-Type'] = $this->_build_params['ctype']; + } + + $encodedHeaders = $this->_encodeHeaders($headers); return $encodedHeaders; } @@ -799,24 +1092,91 @@ class Mail_mime * 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. + * @param array $xtra_headers Assoc array with any extra headers (optional) + * (Don't set Content-Type for multipart messages here!) + * @param bool $overwrite Overwrite the existing headers with new. + * @param bool $skip_content Don't return content headers: Content-Type, + * Content-Disposition and Content-Transfer-Encoding * - * @return string Plain text headers + * @return string Plain text headers * @access public */ - function txtHeaders($xtra_headers = null, $overwrite = false) + function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = false) { - $headers = $this->headers($xtra_headers, $overwrite); - + $headers = $this->headers($xtra_headers, $overwrite, $skip_content); + + // Place Received: headers at the beginning of the message + // Spam detectors often flag messages with it after the Subject: as spam + if (isset($headers['Received'])) { + $received = $headers['Received']; + unset($headers['Received']); + $headers = array('Received' => $received) + $headers; + } + $ret = ''; + $eol = $this->_build_params['eol']; + foreach ($headers as $key => $val) { - $ret .= "$key: $val" . MAIL_MIME_CRLF; + if (is_array($val)) { + foreach ($val as $value) { + $ret .= "$key: $value" . $eol; + } + } else { + $ret .= "$key: $val" . $eol; + } } + return $ret; } + /** + * Sets message Content-Type header. + * Use it to build messages with various content-types e.g. miltipart/raport + * not supported by _contentHeaders() function. + * + * @param string $type Type name + * @param array $params Hash array of header parameters + * + * @return void + * @access public + * @since 1.7.0 + */ + function setContentType($type, $params = array()) + { + $header = $type; + + $eol = !empty($this->_build_params['eol']) + ? $this->_build_params['eol'] : "\r\n"; + + // add parameters + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (is_array($params)) { + foreach ($params as $name => $value) { + if ($name == 'boundary') { + $this->_build_params['boundary'] = $value; + } + if (!preg_match($token_regexp, $value)) { + $header .= ";$eol $name=$value"; + } else { + $value = addcslashes($value, '\\"'); + $header .= ";$eol $name=\"$value\""; + } + } + } + + // add required boundary parameter if not defined + if (preg_match('/^multipart\//i', $type)) { + if (empty($this->_build_params['boundary'])) { + $this->_build_params['boundary'] = '=_' . md5(rand() . microtime()); + } + + $header .= ";$eol boundary=\"".$this->_build_params['boundary']."\""; + } + + $this->_build_params['ctype'] = $header; + } + /** * Sets the Subject header * @@ -843,6 +1203,24 @@ class Mail_mime $this->_headers['From'] = $email; } + /** + * Add an email to the To header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addTo($email) + { + if (isset($this->_headers['To'])) { + $this->_headers['To'] .= ", $email"; + } else { + $this->_headers['To'] = $email; + } + } + /** * Add an email to the Cc (carbon copy) header * (multiple calls to this method are allowed) @@ -880,7 +1258,7 @@ class Mail_mime } /** - * Since the PHP send function requires you to specifiy + * Since the PHP send function requires you to specify * 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 @@ -889,7 +1267,7 @@ class Mail_mime * * @param string $recipients A comma-delimited list of recipients * - * @return string Encoded data + * @return string Encoded data * @access public */ function encodeRecipients($recipients) @@ -900,196 +1278,199 @@ class Mail_mime } /** - * Encodes a header as per RFC2047 + * Encodes headers as per RFC2047 * * @param array $input The header data to encode * @param array $params Extra build parameters * - * @return array Encoded data + * @return array Encoded data * @access private */ function _encodeHeaders($input, $params = array()) { - $build_params = $this->_build_params; while (list($key, $value) = each($params)) { $build_params[$key] = $value; } - //$hdr_name: Name of the heaer - //$hdr_value: Full line of header value. - //$hdr_value_out: The recombined $hdr_val-atoms, or the encoded string. - - $useIconv = true; - if (isset($build_params['ignore-iconv'])) { - $useIconv = !$build_params['ignore-iconv']; - } + foreach ($input as $hdr_name => $hdr_value) { - if (preg_match('#([\x80-\xFF]){1}#', $hdr_value)) { - if (function_exists('iconv_mime_encode') && $useIconv) { - $imePrefs = array(); - if ($build_params['head_encoding'] == 'base64') { - $imePrefs['scheme'] = 'B'; - } else { - $imePrefs['scheme'] = 'Q'; - } - $imePrefs['input-charset'] = $build_params['head_charset']; - $imePrefs['output-charset'] = $build_params['head_charset']; - $imePrefs['line-length'] = 74; - $imePrefs['line-break-chars'] = "\r\n"; //Specified in RFC2047 - - $hdr_value = iconv_mime_encode($hdr_name, $hdr_value, $imePrefs); - $hdr_value = preg_replace("#^{$hdr_name}\:\ #", "", $hdr_value); - } elseif ($build_params['head_encoding'] == 'base64') { - //Base64 encoding has been selected. - //Base64 encode the entire string - $hdr_value = base64_encode($hdr_value); - - //Generate the header using the specified params and dynamicly - //determine the maximum length of such strings. - //75 is the value specified in the RFC. The first -2 is there so - //the later regexp doesn't break any of the translated chars. - //The -2 on the first line-regexp is to compensate for the ": " - //between the header-name and the header value - $prefix = '=?' . $build_params['head_charset'] . '?B?'; - $suffix = '?='; - $maxLength = 75 - strlen($prefix . $suffix) - 2; - $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; - - //We can cut base4 every 4 characters, so the real max - //we can get must be rounded down. - $maxLength = $maxLength - ($maxLength % 4); - $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); - - $cutpoint = $maxLength1stLine; - $hdr_value_out = $hdr_value; - $output = ""; - while ($hdr_value_out) { - //Split translated string at every $maxLength - $part = substr($hdr_value_out, 0, $cutpoint); - $hdr_value_out = substr($hdr_value_out, $cutpoint); - $cutpoint = $maxLength; - //RFC 2047 specifies that any split header should - //be seperated by a CRLF SPACE. - if ($output) { - $output .= "\r\n "; - } - $output .= $prefix . $part . $suffix; - } - $hdr_value = $output; - } else { - //quoted-printable encoding has been selected - - //Fix for Bug #10298, Ota Mares <om@viazenetti.de> - //Check if there is a double quote at beginning or end of - //the string to prevent that an open or closing quote gets - //ignored because it is encapsuled by an encoding pre/suffix. - //Remove the double quote and set the specific prefix or - //suffix variable so that we can concat the encoded string and - //the double quotes back together to get the intended string. - $quotePrefix = $quoteSuffix = ''; - if ($hdr_value{0} == '"') { - $hdr_value = substr($hdr_value, 1); - $quotePrefix = '"'; - } - if ($hdr_value{strlen($hdr_value)-1} == '"') { - $hdr_value = substr($hdr_value, 0, -1); - $quoteSuffix = '"'; - } - - //Generate the header using the specified params and dynamicly - //determine the maximum length of such strings. - //75 is the value specified in the RFC. The -2 is there so - //the later regexp doesn't break any of the translated chars. - //The -2 on the first line-regexp is to compensate for the ": " - //between the header-name and the header value - $prefix = '=?' . $build_params['head_charset'] . '?Q?'; - $suffix = '?='; - $maxLength = 75 - strlen($prefix . $suffix) - 2 - 1; - $maxLength1stLine = $maxLength - strlen($hdr_name) - 2; - $maxLength = $maxLength - 1; - - //Replace all special characters used by the encoder. - $search = array('=', '_', '?', ' '); - $replace = array('=3D', '=5F', '=3F', '_'); - $hdr_value = str_replace($search, $replace, $hdr_value); - - //Replace all extended characters (\x80-xFF) with their - //ASCII values. - $hdr_value = preg_replace('#([\x80-\xFF])#e', - '"=" . strtoupper(dechex(ord("\1")))', - $hdr_value); - - //This regexp will break QP-encoded text at every $maxLength - //but will not break any encoded letters. - $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; - $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; - //Fix for Bug #10298, Ota Mares <om@viazenetti.de> - //Concat the double quotes and encoded string together - $hdr_value = $quotePrefix . $hdr_value . $quoteSuffix; - - - $hdr_value_out = $hdr_value; - $realMax = $maxLength1stLine + strlen($prefix . $suffix); - if (strlen($hdr_value_out) >= $realMax) { - //Begin with the regexp for the first line. - $reg = $reg1st; - $output = ""; - while ($hdr_value_out) { - //Split translated string at every $maxLength - //But make sure not to break any translated chars. - $found = preg_match($reg, $hdr_value_out, $matches); - - //After this first line, we need to use a different - //regexp for the first line. - $reg = $reg2nd; - - //Save the found part and encapsulate it in the - //prefix & suffix. Then remove the part from the - //$hdr_value_out variable. - if ($found) { - $part = $matches[0]; - $len = strlen($matches[0]); - $hdr_value_out = substr($hdr_value_out, $len); - } else { - $part = $hdr_value_out; - $hdr_value_out = ""; - } - - //RFC 2047 specifies that any split header should - //be seperated by a CRLF SPACE - if ($output) { - $output .= "\r\n "; - } - $output .= $prefix . $part . $suffix; - } - $hdr_value_out = $output; - } else { - $hdr_value_out = $prefix . $hdr_value_out . $suffix; - } - $hdr_value = $hdr_value_out; + if (is_array($hdr_value)) { + foreach ($hdr_value as $idx => $value) { + $input[$hdr_name][$idx] = $this->encodeHeader( + $hdr_name, $value, + $build_params['head_charset'], $build_params['head_encoding'] + ); } + } else { + $input[$hdr_name] = $this->encodeHeader( + $hdr_name, $hdr_value, + $build_params['head_charset'], $build_params['head_encoding'] + ); } - $input[$hdr_name] = $hdr_value; } + return $input; } /** - * Set the object's end-of-line and define the constant if applicable. + * Encodes a header as per RFC2047 * - * @param string $eol End Of Line sequence + * @param string $name The header name + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) * - * @return void + * @return string Encoded header data (without a name) + * @access public + * @since 1.5.3 + */ + function encodeHeader($name, $value, $charset, $encoding) + { + $mime_part = new Mail_mimePart; + return $mime_part->encodeHeader( + $name, $value, $charset, $encoding, $this->_build_params['eol'] + ); + } + + /** + * Get file's basename (locale independent) + * + * @param string $filename Filename + * + * @return string Basename * @access private */ - function _setEOL($eol) + function _basename($filename) { - $this->_eol = $eol; - if (!defined('MAIL_MIME_CRLF')) { - define('MAIL_MIME_CRLF', $this->_eol, true); + // basename() is not unicode safe and locale dependent + if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) { + return preg_replace('/^.*[\\\\\\/]/', '', $filename); + } else { + return preg_replace('/^.*[\/]/', '', $filename); } } - + /** + * Get Content-Type and Content-Transfer-Encoding headers of the message + * + * @return array Headers array + * @access private + */ + function _contentHeaders() + { + $attachments = count($this->_parts) ? true : false; + $html_images = count($this->_html_images) ? true : false; + $html = strlen($this->_htmlbody) ? true : false; + $text = (!$html && strlen($this->_txtbody)) ? true : false; + $headers = array(); + + // See get() + switch (true) { + case $text && !$attachments: + $headers['Content-Type'] = 'text/plain'; + break; + + case !$text && !$html && $attachments: + case $text && $attachments: + case $html && $attachments && !$html_images: + case $html && $attachments && $html_images: + $headers['Content-Type'] = 'multipart/mixed'; + break; + + case $html && !$attachments && !$html_images && isset($this->_txtbody): + case $html && !$attachments && $html_images && isset($this->_txtbody): + $headers['Content-Type'] = 'multipart/alternative'; + break; + + case $html && !$attachments && !$html_images && !isset($this->_txtbody): + $headers['Content-Type'] = 'text/html'; + break; + + case $html && !$attachments && $html_images && !isset($this->_txtbody): + $headers['Content-Type'] = 'multipart/related'; + break; + + default: + return $headers; + } + + $this->_checkParams(); + + $eol = !empty($this->_build_params['eol']) + ? $this->_build_params['eol'] : "\r\n"; + + if ($headers['Content-Type'] == 'text/plain') { + // single-part message: add charset and encoding + $charset = 'charset=' . $this->_build_params['text_charset']; + // place charset parameter in the same line, if possible + // 26 = strlen("Content-Type: text/plain; ") + $headers['Content-Type'] + .= (strlen($charset) + 26 <= 76) ? "; $charset" : ";$eol $charset"; + $headers['Content-Transfer-Encoding'] + = $this->_build_params['text_encoding']; + } else if ($headers['Content-Type'] == 'text/html') { + // single-part message: add charset and encoding + $charset = 'charset=' . $this->_build_params['html_charset']; + // place charset parameter in the same line, if possible + $headers['Content-Type'] + .= (strlen($charset) + 25 <= 76) ? "; $charset" : ";$eol $charset"; + $headers['Content-Transfer-Encoding'] + = $this->_build_params['html_encoding']; + } else { + // multipart message: and boundary + if (!empty($this->_build_params['boundary'])) { + $boundary = $this->_build_params['boundary']; + } else if (!empty($this->_headers['Content-Type']) + && preg_match('/boundary="([^"]+)"/', $this->_headers['Content-Type'], $m) + ) { + $boundary = $m[1]; + } else { + $boundary = '=_' . md5(rand() . microtime()); + } + + $this->_build_params['boundary'] = $boundary; + $headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + } + + return $headers; + } + + /** + * Validate and set build parameters + * + * @return void + * @access private + */ + function _checkParams() + { + $encodings = array('7bit', '8bit', 'base64', 'quoted-printable'); + + $this->_build_params['text_encoding'] + = strtolower($this->_build_params['text_encoding']); + $this->_build_params['html_encoding'] + = strtolower($this->_build_params['html_encoding']); + + if (!in_array($this->_build_params['text_encoding'], $encodings)) { + $this->_build_params['text_encoding'] = '7bit'; + } + if (!in_array($this->_build_params['html_encoding'], $encodings)) { + $this->_build_params['html_encoding'] = '7bit'; + } + + // text body + if ($this->_build_params['text_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->_build_params['text_charset']) + && preg_match('/[^\x00-\x7F]/', $this->_txtbody) + ) { + $this->_build_params['text_encoding'] = 'quoted-printable'; + } + // html body + if ($this->_build_params['html_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->_build_params['html_charset']) + && preg_match('/[^\x00-\x7F]/', $this->_htmlbody) + ) { + $this->_build_params['html_encoding'] = 'quoted-printable'; + } + } } // End of class diff --git a/include/pear/Mail/mimePart.php b/include/pear/Mail/mimePart.php index 7ad54e5ef58d1c6ee919b310af05dbfa38bf7a8b..a839402f7a6648c1ffe6ef61284770190c8933f1 100644 --- a/include/pear/Mail/mimePart.php +++ b/include/pear/Mail/mimePart.php @@ -40,15 +40,16 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. * - * @category Mail - * @package Mail_Mime - * @author Richard Heyes <richard@phpguru.org> - * @author Cipriano Groenendal <cipri@php.net> - * @author Sean Coates <sean@php.net> - * @copyright 2003-2006 PEAR <pear-group@php.net> - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version CVS: $Id: mimePart.php,v 1.25 2007/05/14 21:43:08 cipri Exp $ - * @link http://pear.php.net/package/Mail_mime + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Mail_mime */ @@ -61,19 +62,20 @@ * of mime mail. * This class however allows full control over the email. * - * @category Mail - * @package Mail_Mime - * @author Richard Heyes <richard@phpguru.org> - * @author Cipriano Groenendal <cipri@php.net> - * @author Sean Coates <sean@php.net> - * @copyright 2003-2006 PEAR <pear-group@php.net> - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://pear.php.net/package/Mail_mime + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime */ -class Mail_mimePart { - - /** +class Mail_mimePart +{ + /** * The encoding type of this part * * @var string @@ -81,7 +83,7 @@ class Mail_mimePart { */ var $_encoding; - /** + /** * An array of subparts * * @var array @@ -89,7 +91,7 @@ class Mail_mimePart { */ var $_subparts; - /** + /** * The output of this part after being built * * @var string @@ -97,7 +99,7 @@ class Mail_mimePart { */ var $_encoded; - /** + /** * Headers for this part * * @var array @@ -105,7 +107,7 @@ class Mail_mimePart { */ var $_headers; - /** + /** * The body of this part (not encoded) * * @var string @@ -114,106 +116,169 @@ class Mail_mimePart { 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 - */ + * The location of file with body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body_file; + + /** + * The end-of-line sequence + * + * @var string + * @access private + */ + var $_eol = "\r\n"; + + + /** + * Constructor. + * + * Sets up the object. + * + * @param string $body The body of the mime part if any. + * @param array $params An associative array of optional parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, + * base64, or quoted-printable + * charset - Content character set + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * filename - Filename parameter for content disposition + * description - Content description + * name_encoding - Encoding of the attachment name (Content-Type) + * By default filenames are encoded using RFC2231 + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * filename_encoding - Encoding of the attachment filename (Content-Disposition) + * See 'name_encoding' + * headers_charset - Charset of the headers e.g. filename, description. + * If not set, 'charset' will be used + * eol - End of line sequence. Default: "\r\n" + * headers - Hash array with additional part headers. Array keys can be + * in form of <header_name>:<parameter_name> + * body_file - Location of file with part's body (instead of $body) + * + * @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); + if (!empty($params['eol'])) { + $this->_eol = $params['eol']; + } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat. + $this->_eol = MAIL_MIMEPART_CRLF; + } + + // Additional part headers + if (!empty($params['headers']) && is_array($params['headers'])) { + $headers = $params['headers']; } - $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; + case 'encoding': + $this->_encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'location': + $headers['Content-Location'] = $value; + break; + + case 'body_file': + $this->_body_file = $value; + break; + + // for backward compatibility + case 'dfilename': + $params['filename'] = $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']}\""; + + // Default content-type + if (empty($params['content_type'])) { + $params['content_type'] = 'text/plain'; + } + + // Content-Type + $headers['Content-Type'] = $params['content_type']; + if (!empty($params['charset'])) { + $charset = "charset={$params['charset']}"; + // place charset parameter in the same line, if possible + if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { + $headers['Content-Type'] .= '; '; + } else { + $headers['Content-Type'] .= ';' . $this->_eol . ' '; } + $headers['Content-Type'] .= $charset; + + // Default headers charset + if (!isset($params['headers_charset'])) { + $params['headers_charset'] = $params['charset']; + } + } + + // header values encoding parameters + $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII'; + $h_language = !empty($params['language']) ? $params['language'] : null; + $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null; + + + if (!empty($params['filename'])) { + $headers['Content-Type'] .= ';' . $this->_eol; + $headers['Content-Type'] .= $this->_buildHeaderParam( + 'name', $params['filename'], $h_charset, $h_language, $h_encoding + ); } + // Content-Disposition + if (!empty($params['disposition'])) { + $headers['Content-Disposition'] = $params['disposition']; + if (!empty($params['filename'])) { + $headers['Content-Disposition'] .= ';' . $this->_eol; + $headers['Content-Disposition'] .= $this->_buildHeaderParam( + 'filename', $params['filename'], $h_charset, $h_language, + !empty($params['filename_encoding']) ? $params['filename_encoding'] : null + ); + } - 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); + // add attachment size + $size = $this->_body_file ? filesize($this->_body_file) : strlen($body); + if ($size) { + $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size; } } - - - - - // Default content-type - if (!isset($headers['Content-Type'])) { - $headers['Content-Type'] = 'text/plain'; + + if (!empty($params['description'])) { + $headers['Content-Description'] = $this->encodeHeader( + 'Content-Description', $params['description'], $h_charset, $h_encoding, + $this->_eol + ); + } + + // Search and add existing headers' parameters + foreach ($headers as $key => $value) { + $items = explode(':', $key); + if (count($items) == 2) { + $header = $items[0]; + $param = $items[1]; + if (isset($headers[$header])) { + $headers[$header] .= ';' . $this->_eol; + } + $headers[$header] .= $this->_buildHeaderParam( + $param, $value, $h_charset, $h_language, $h_encoding + ); + unset($headers[$key]); + } } - //Default encoding + // Default encoding if (!isset($this->_encoding)) { $this->_encoding = '7bit'; } @@ -225,41 +290,60 @@ class Mail_mimePart { } /** - * encode() - * * Encodes and returns the email. Also stores * it in the encoded member variable * + * @param string $boundary Pre-defined boundary string + * * @return An associative array containing two elements, * body and headers. The headers element is itself - * an indexed array. + * an indexed array. On error returns PEAR error object. * @access public */ - function encode() + function encode($boundary=null) { $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 . '"'; + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $eol = $this->_eol; + + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + + $encoded['body'] = ''; - // Add body parts to $subparts for ($i = 0; $i < count($this->_subparts); $i++) { - $headers = array(); + $encoded['body'] .= '--' . $boundary . $eol; $tmp = $this->_subparts[$i]->encode(); + if (PEAR::isError($tmp)) { + return $tmp; + } foreach ($tmp['headers'] as $key => $value) { - $headers[] = $key . ': ' . $value; + $encoded['body'] .= $key . ': ' . $value . $eol; } - $subparts[] = implode(MAIL_MIMEPART_CRLF, $headers) . MAIL_MIMEPART_CRLF . MAIL_MIMEPART_CRLF . $tmp['body'] . MAIL_MIMEPART_CRLF; + $encoded['body'] .= $eol . $tmp['body'] . $eol; } - $encoded['body'] = '--' . $boundary . MAIL_MIMEPART_CRLF . - rtrim(implode('--' . $boundary . MAIL_MIMEPART_CRLF , $subparts), MAIL_MIMEPART_CRLF) . MAIL_MIMEPART_CRLF . - '--' . $boundary.'--' . MAIL_MIMEPART_CRLF; + $encoded['body'] .= '--' . $boundary . '--' . $eol; - } else { + } else if ($this->_body) { $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); + } else if ($this->_body_file) { + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + if (PEAR::isError($body)) { + return $body; + } + $encoded['body'] = $body; + } else { + $encoded['body'] = ''; } // Add headers to $encoded @@ -269,154 +353,356 @@ class Mail_mimePart { } /** - * &addSubPart() + * Encodes and saves the email into file. File must exist. + * Data will be appended to the file. + * + * @param string $filename Output file location + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers + * + * @return array An associative array containing message headers + * or PEAR error object + * @access public + * @since 1.6.0 + */ + function encodeToFile($filename, $boundary=null, $skip_head=false) + { + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writeable: ' . $filename); + return $err; + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + $res = $this->_encodePartToFile($fh, $boundary, $skip_head); + + fclose($fh); + + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + return PEAR::isError($res) ? $res : $this->_headers; + } + + /** + * Encodes given email part into file + * + * @param string $fh Output file handle + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers * + * @return array True on sucess or PEAR error object + * @access private + */ + function _encodePartToFile($fh, $boundary=null, $skip_head=false) + { + $eol = $this->_eol; + + if (count($this->_subparts)) { + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + } + + if (!$skip_head) { + foreach ($this->_headers as $key => $value) { + fwrite($fh, $key . ': ' . $value . $eol); + } + $f_eol = $eol; + } else { + $f_eol = ''; + } + + if (count($this->_subparts)) { + for ($i = 0; $i < count($this->_subparts); $i++) { + fwrite($fh, $f_eol . '--' . $boundary . $eol); + $res = $this->_subparts[$i]->_encodePartToFile($fh); + if (PEAR::isError($res)) { + return $res; + } + $f_eol = $eol; + } + + fwrite($fh, $eol . '--' . $boundary . '--' . $eol); + + } else if ($this->_body) { + fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding)); + } else if ($this->_body_file) { + fwrite($fh, $f_eol); + $res = $this->_getEncodedDataFromFile( + $this->_body_file, $this->_encoding, $fh + ); + if (PEAR::isError($res)) { + return $res; + } + } + + return true; + } + + /** * 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. + * @param string $body The body of the subpart, if any. + * @param array $params The parameters for the subpart, same + * as the $params argument for constructor. + * + * @return Mail_mimePart 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) + 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. + * @param string $data The data to encode. + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * + * @return string * @access private */ function _getEncodedData($data, $encoding) { switch ($encoding) { - case '8bit': - case '7bit': - return $data; - break; + case 'quoted-printable': + return $this->_quotedPrintableEncode($data); + break; - case 'quoted-printable': - return $this->_quotedPrintableEncode($data); - break; - - case 'base64': - return rtrim(chunk_split(base64_encode($data), 76, MAIL_MIMEPART_CRLF)); - break; + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, $this->_eol)); + break; - default: - return $data; + case '8bit': + case '7bit': + default: + return $data; } } /** - * quotedPrintableEncode() + * Returns encoded data based upon encoding passed to it * + * @param string $filename Data file location + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * @param resource $fh Output file handle. If set, data will be + * stored into it instead of returning it + * + * @return string Encoded data or PEAR error object + * @access private + */ + function _getEncodedDataFromFile($filename, $encoding, $fh=null) + { + if (!is_readable($filename)) { + $err = PEAR::raiseError('Unable to read file: ' . $filename); + return $err; + } + + if (!($fd = fopen($filename, 'rb'))) { + $err = PEAR::raiseError('Could not open file: ' . $filename); + return $err; + } + + $data = ''; + + switch ($encoding) { + case 'quoted-printable': + while (!feof($fd)) { + $buffer = $this->_quotedPrintableEncode(fgets($fd)); + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case 'base64': + while (!feof($fd)) { + // Should read in a multiple of 57 bytes so that + // the output is 76 bytes per line. Don't use big chunks + // because base64 encoding is memory expensive + $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB + $buffer = base64_encode($buffer); + $buffer = chunk_split($buffer, 76, $this->_eol); + if (feof($fd)) { + $buffer = rtrim($buffer); + } + + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case '8bit': + case '7bit': + default: + while (!feof($fd)) { + $buffer = fread($fd, 1048576); // 1 MB + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + } + + fclose($fd); + + if (!$fh) { + return $data; + } + } + + /** * 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 + * @param string $input The data to encode + * @param int $line_max Optional max line length. Should + * not be more than 76 chars + * + * @return string Encoded data * * @access private */ function _quotedPrintableEncode($input , $line_max = 76) { + $eol = $this->_eol; + /* + // imap_8bit() is extremely fast, but doesn't handle properly some characters + if (function_exists('imap_8bit') && $line_max == 76) { + $input = preg_replace('/\r?\n/', "\r\n", $input); + $input = imap_8bit($input); + if ($eol != "\r\n") { + $input = str_replace("\r\n", $eol, $input); + } + return $input; + } + */ $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); + while (list($idx, $line) = each($lines)) { $newline = ''; + $i = 0; - for ($i = 0; $i < $linlen; $i++) { + while (isset($line[$i])) { $char = $line[$i]; $dec = ord($char); + $i++; - if (($dec == 32) AND ($i == ($linlen - 1))) { // convert space at eol only + if (($dec == 32) && (!isset($line[$i]))) { + // 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) + } elseif ($dec == 9 && isset($line[$i])) { + ; // Do nothing if a TAB is not on eol + } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) { + $char = $escape . sprintf('%02X', $dec); + } elseif (($dec == 46) && (($newline == '') + || ((strlen($newline) + strlen("=2E")) >= $line_max)) + ) { + // Bug #9722: convert full-stop at bol, + // some Windows servers need this, won't break anything (cipri) + // Bug #11731: full-stop at bol also needs to be encoded + // if this line would push us over the line_max limit. $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 + // Note, when changing this line, also change the ($dec == 46) + // check line, as it mimics this line due to Bug #11731 + // EOL is not counted + if ((strlen($newline) + strlen($char)) >= $line_max) { + // soft line break; " =\r\n" is okay + $output .= $newline . $escape . $eol; $newline = ''; } $newline .= $char; } // end of for $output .= $newline . $eol; + unset($lines[$idx]); } - $output = substr($output, 0, -1 * strlen($eol)); // Don't want last crlf + // Don't want last crlf + $output = substr($output, 0, -1 * strlen($eol)); 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 + * @param string $name The name of the header-parameter + * @param string $value The value of the paramter + * @param string $charset The characterset of $value + * @param string $language The language used in $value + * @param string $encoding Parameter encoding. If not set, parameter value + * is encoded according to RFC2231 + * @param int $maxLength The maximum length of a line. Defauls to 75 + * + * @return string * * @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}\"; "; + function _buildHeaderParam($name, $value, $charset=null, $language=null, + $encoding=null, $maxLength=75 + ) { + // RFC 2045: + // value needs encoding if contains non-ASCII chars or is longer than 78 chars + if (!preg_match('#[^\x20-\x7E]#', $value)) { + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (!preg_match($token_regexp, $value)) { + // token + if (strlen($name) + strlen($value) + 3 <= $maxLength) { + return " {$name}={$value}"; + } + } else { + // quoted-string + $quoted = addcslashes($value, '\\"'); + if (strlen($name) + strlen($quoted) + 5 <= $maxLength) { + return " {$name}=\"{$quoted}\""; + } + } + } + + // RFC2047: use quoted-printable/base64 encoding + if ($encoding == 'quoted-printable' || $encoding == 'base64') { + return $this->_buildRFC2047Param($name, $value, $charset, $encoding); + } + + // RFC2231: + $encValue = preg_replace_callback( + '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/', + array($this, '_encodeReplaceCallback'), $value + ); + $value = "$charset'$language'$encValue"; + + $header = " {$name}*={$value}"; if (strlen($header) <= $maxLength) { return $header; } - $preLength = strlen(" {$name}*0{$secondAsterisk}=\""); - $sufLength = strlen("\";"); - $maxLength = MAX(16, $maxLength - $preLength - $sufLength - 2); + $preLength = strlen(" {$name}*0*="); + $maxLength = max(16, $maxLength - $preLength - 3); $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; $headers = array(); @@ -425,15 +711,518 @@ class Mail_mimePart { $matches = array(); $found = preg_match($maxLengthReg, $value, $matches); if ($found) { - $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$matches[0]}\""; + $headers[] = " {$name}*{$headCount}*={$matches[0]}"; $value = substr($value, strlen($matches[0])); } else { - $headers[] = " {$name}*{$headCount}{$secondAsterisk}=\"{$value}\""; - $value = ""; + $headers[] = " {$name}*{$headCount}*={$value}"; + $value = ''; } $headCount++; } - $headers = implode(MAIL_MIMEPART_CRLF, $headers) . ';'; + + $headers = implode(';' . $this->_eol, $headers); return $headers; } + + /** + * Encodes header parameter as per RFC2047 if needed + * + * @param string $name The parameter name + * @param string $value The parameter value + * @param string $charset The parameter charset + * @param string $encoding Encoding type (quoted-printable or base64) + * @param int $maxLength Encoded parameter max length. Default: 76 + * + * @return string Parameter line + * @access private + */ + function _buildRFC2047Param($name, $value, $charset, + $encoding='quoted-printable', $maxLength=76 + ) { + // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in + // parameter of a MIME Content-Type or Content-Disposition field", + // but... it's supported by many clients/servers + $quoted = ''; + + if ($encoding == 'base64') { + $value = base64_encode($value); + $prefix = '=?' . $charset . '?B?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + // We can cut base64-encoded string every 4 characters + $real_len = floor(($maxLength - $add_len) / 4) * 4; + $_quote = substr($value, 0, $real_len); + $value = substr($value, $real_len); + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + $quoted .= $prefix . $value . $suffix; + + } else { + // quoted-printable + $value = $this->encodeQP($value); + $prefix = '=?' . $charset . '?Q?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + $length = $maxLength - $add_len; + // don't break any encoded letters + if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) { + $_quote = $matches[1]; + } + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $value = substr($value, strlen($_quote)); + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + + $quoted .= $prefix . $value . $suffix; + } + + return " {$name}=\"{$quoted}\""; + } + + /** + * Encodes a header as per RFC2047 + * + * @param string $name The header name + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data (without a name) + * @access public + * @since 1.6.1 + */ + function encodeHeader($name, $value, $charset='ISO-8859-1', + $encoding='quoted-printable', $eol="\r\n" + ) { + // Structured headers + $comma_headers = array( + 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to', + 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc', + 'resent-sender', 'resent-reply-to', + 'return-receipt-to', 'disposition-notification-to', + ); + $other_headers = array( + 'references', 'in-reply-to', 'message-id', 'resent-message-id', + ); + + $name = strtolower($name); + + if (in_array($name, $comma_headers)) { + $separator = ','; + } else if (in_array($name, $other_headers)) { + $separator = ' '; + } + + if (!$charset) { + $charset = 'ISO-8859-1'; + } + + // Structured header (make sure addr-spec inside is not encoded) + if (!empty($separator)) { + // Simple e-mail address regexp + $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+'; + + $parts = Mail_mimePart::_explodeQuotedString($separator, $value); + $value = ''; + + foreach ($parts as $part) { + $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part); + $part = trim($part); + + if (!$part) { + continue; + } + if ($value) { + $value .= $separator==',' ? $separator.' ' : ' '; + } else { + $value = $name . ': '; + } + + // let's find phrase (name) and/or addr-spec + if (preg_match('/^<' . $email_regexp . '>$/', $part)) { + $value .= $part; + } else if (preg_match('/^' . $email_regexp . '$/', $part)) { + // address without brackets and without name + $value .= $part; + } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) { + // address with name (handle name) + $address = $matches[0]; + $word = str_replace($address, '', $part); + $word = trim($word); + // check if phrase requires quoting + if ($word) { + // non-ASCII: require encoding + if (preg_match('#([\x80-\xFF]){1}#', $word)) { + if ($word[0] == '"' && $word[strlen($word)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $word = str_replace($search, $replace, $word); + $word = substr($word, 1, -1); + } + // find length of last line + if (($pos = strrpos($value, $eol)) !== false) { + $last_len = strlen($value) - $pos; + } else { + $last_len = strlen($value); + } + $word = Mail_mimePart::encodeHeaderValue( + $word, $charset, $encoding, $last_len, $eol + ); + } else if (($word[0] != '"' || $word[strlen($word)-1] != '"') + && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word) + ) { + // ASCII: quote string if needed + $word = '"'.addcslashes($word, '\\"').'"'; + } + } + $value .= $word.' '.$address; + } else { + // addr-spec not found, don't encode (?) + $value .= $part; + } + + // RFC2822 recommends 78 characters limit, use 76 from RFC2047 + $value = wordwrap($value, 76, $eol . ' '); + } + + // remove header name prefix (there could be EOL too) + $value = preg_replace( + '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value + ); + + } else { + // Unstructured header + // non-ASCII: require encoding + if (preg_match('#([\x80-\xFF]){1}#', $value)) { + if ($value[0] == '"' && $value[strlen($value)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $value = str_replace($search, $replace, $value); + $value = substr($value, 1, -1); + } + $value = Mail_mimePart::encodeHeaderValue( + $value, $charset, $encoding, strlen($name) + 2, $eol + ); + } else if (strlen($name.': '.$value) > 78) { + // ASCII: check if header line isn't too long and use folding + $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value); + $tmp = wordwrap($name.': '.$value, 78, $eol . ' '); + $value = preg_replace('/^'.$name.':\s*/', '', $tmp); + // hard limit 998 (RFC2822) + $value = wordwrap($value, 998, $eol . ' ', true); + } + } + + return $value; + } + + /** + * Explode quoted string + * + * @param string $delimiter Delimiter expression string for preg_match() + * @param string $string Input string + * + * @return array String tokens array + * @access private + */ + function _explodeQuotedString($delimiter, $string) + { + $result = array(); + $strlen = strlen($string); + + for ($q=$p=$i=0; $i < $strlen; $i++) { + if ($string[$i] == "\"" + && (empty($string[$i-1]) || $string[$i-1] != "\\") + ) { + $q = $q ? false : true; + } else if (!$q && preg_match("/$delimiter/", $string[$i])) { + $result[] = substr($string, $p, $i - $p); + $p = $i + 1; + } + } + + $result[] = substr($string, $p); + return $result; + } + + /** + * Encodes a header value as per RFC2047 + * + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data + * @access public + * @since 1.6.1 + */ + function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + // #17311: Use multibyte aware method (requires mbstring extension) + if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) { + return $result; + } + + // Generate the header using the specified params and dynamicly + // determine the maximum length of such strings. + // 75 is the value specified in the RFC. + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + $prefix = '=?' . $charset . '?' . $encoding .'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + $maxLength1stLine = $maxLength - $prefix_len; + + if ($encoding == 'B') { + // Base64 encode the entire string + $value = base64_encode($value); + + // We can cut base64 every 4 characters, so the real max + // we can get must be rounded down. + $maxLength = $maxLength - ($maxLength % 4); + $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); + + $cutpoint = $maxLength1stLine; + $output = ''; + + while ($value) { + // Split translated string at every $maxLength + $part = substr($value, 0, $cutpoint); + $value = substr($value, $cutpoint); + $cutpoint = $maxLength; + // RFC 2047 specifies that any split header should + // be seperated by a CRLF SPACE. + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + // quoted-printable encoding has been selected + $value = Mail_mimePart::encodeQP($value); + + // This regexp will break QP-encoded text at every $maxLength + // but will not break any encoded letters. + $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; + $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; + + if (strlen($value) > $maxLength1stLine) { + // Begin with the regexp for the first line. + $reg = $reg1st; + $output = ''; + while ($value) { + // Split translated string at every $maxLength + // But make sure not to break any translated chars. + $found = preg_match($reg, $value, $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 + // $value_out variable. + if ($found) { + $part = $matches[0]; + $len = strlen($matches[0]); + $value = substr($value, $len); + } else { + $part = $value; + $value = ''; + } + + // RFC 2047 specifies that any split header should + // be seperated by a CRLF SPACE + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + $value = $prefix . $value . $suffix; + } + } + + return $value; + } + + /** + * Encodes the given string using quoted-printable + * + * @param string $str String to encode + * + * @return string Encoded string + * @access public + * @since 1.6.0 + */ + function encodeQP($str) + { + // Bug #17226 RFC 2047 restricts some characters + // if the word is inside a phrase, permitted chars are only: + // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_" + + // "=", "_", "?" must be encoded + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + $str = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str + ); + + return str_replace(' ', '_', $str); + } + + /** + * Encodes the given string using base64 or quoted-printable. + * This method makes sure that encoded-word represents an integral + * number of characters as per RFC2047. + * + * @param string $str String to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded string + * @access public + * @since 1.8.0 + */ + function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + if (!function_exists('mb_substr') || !function_exists('mb_strlen')) { + return; + } + + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + // 75 is the value specified in the RFC + $prefix = '=?' . $charset . '?'.$encoding.'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + + // A multi-octet character may not be split across adjacent encoded-words + // So, we'll loop over each character + // mb_stlen() with wrong charset will generate a warning here and return null + $length = mb_strlen($str, $charset); + $result = ''; + $line_length = $prefix_len; + + if ($encoding == 'B') { + // base64 + $start = 0; + $prev = ''; + + for ($i=1; $i<=$length; $i++) { + // See #17311 + $chunk = mb_substr($str, $start, $i-$start, $charset); + $chunk = base64_encode($chunk); + $chunk_len = strlen($chunk); + + if ($line_length + $chunk_len == $maxLength || $i == $length) { + if ($result) { + $result .= "\n"; + } + $result .= $chunk; + $line_length = 0; + $start = $i; + } else if ($line_length + $chunk_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + if ($prev) { + $result .= $prev; + } + $line_length = 0; + $start = $i - 1; + } else { + $prev = $chunk; + } + } + } else { + // quoted-printable + // see encodeQP() + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + + for ($i=0; $i<=$length; $i++) { + $char = mb_substr($str, $i, 1, $charset); + // RFC recommends underline (instead of =20) in place of the space + // that's one of the reasons why we're not using iconv_mime_encode() + if ($char == ' ') { + $char = '_'; + $char_len = 1; + } else { + $char = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char + ); + $char_len = strlen($char); + } + + if ($line_length + $char_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + $line_length = 0; + } + + $result .= $char; + $line_length += $char_len; + } + } + + if ($result) { + $result = $prefix + .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix; + } + + return $result; + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2047: quoted-printable) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _qpReplaceCallback($matches) + { + return sprintf('=%02X', ord($matches[1])); + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2231) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _encodeReplaceCallback($matches) + { + return sprintf('%%%02X', ord($matches[1])); + } + } // End of class diff --git a/include/pear/Mail/mock.php b/include/pear/Mail/mock.php index 1dfdadb8bb39e47a0b76446505e0de2e3234210c..61570ba408cdcec0b6cd5814f080d4aa78581a45 100644 --- a/include/pear/Mail/mock.php +++ b/include/pear/Mail/mock.php @@ -1,119 +1,143 @@ -<?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Author: Chuck Hagenbuch <chuck@horde.org> | -// +----------------------------------------------------------------------+ -// -// $Id: 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; - } - -} +<?php +/** + * Mock implementation + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: mock.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +/** + * Mock implementation of the PEAR Mail:: interface for testing. + * @access public + * @package Mail + * @version $Revision: 294747 $ + */ +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 index 982bfa45b6d652132a5219fb273d4327e505e856..f8d58272eea5df5abe4d8e9a25b0f24dee2ba776 100644 --- a/include/pear/Mail/null.php +++ b/include/pear/Mail/null.php @@ -1,29 +1,53 @@ <?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Author: Phil Kernick <philk@rotfl.com.au> | -// +----------------------------------------------------------------------+ -// -// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $ -// +/** + * Null implementation of the PEAR Mail interface + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Phil Kernick + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Phil Kernick <philk@rotfl.com.au> + * @copyright 2010 Phil Kernick + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: null.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ /** * Null implementation of the PEAR Mail:: interface. * @access public * @package Mail - * @version $Revision: 1.2 $ + * @version $Revision: 294747 $ */ class Mail_null extends Mail { diff --git a/include/pear/Mail/sendmail.php b/include/pear/Mail/sendmail.php index 183890a80990851b7e11c6bac6a939a2e603bbf3..b056575e99274f886b1473b5ee35f002d2699329 100644 --- a/include/pear/Mail/sendmail.php +++ b/include/pear/Mail/sendmail.php @@ -1,171 +1,171 @@ -<?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Author: Chuck Hagenbuch <chuck@horde.org> | -// +----------------------------------------------------------------------+ - -/** - * Sendmail implementation of the PEAR Mail:: interface. - * @access public - * @package Mail - * @version $Revision: 1.20 $ - */ -class Mail_sendmail extends Mail { - - /** - * The location of the sendmail or sendmail wrapper binary on the - * filesystem. - * @var string - */ - var $sendmail_path = '/usr/sbin/sendmail'; - - /** - * Any extra command-line parameters to pass to the sendmail or - * sendmail wrapper binary. - * @var string - */ - var $sendmail_args = '-i'; - - /** - * Constructor. - * - * Instantiates a new Mail_sendmail:: object based on the parameters - * passed in. It looks for the following parameters: - * sendmail_path The location of the sendmail binary on the - * filesystem. Defaults to '/usr/sbin/sendmail'. - * - * sendmail_args Any extra parameters to pass to the sendmail - * or sendmail wrapper binary. - * - * If a parameter is present in the $params array, it replaces the - * default. - * - * @param array $params Hash containing any parameters different from the - * defaults. - * @access public - */ - function Mail_sendmail($params) - { - if (isset($params['sendmail_path'])) { - $this->sendmail_path = $params['sendmail_path']; - } - if (isset($params['sendmail_args'])) { - $this->sendmail_args = $params['sendmail_args']; - } - - /* - * Because we need to pass message headers to the sendmail program on - * the commandline, we can't guarantee the use of the standard "\r\n" - * separator. Instead, we use the system's native line separator. - */ - if (defined('PHP_EOL')) { - $this->sep = PHP_EOL; - } else { - $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; - } - } - - /** - * Implements Mail::send() function using the sendmail - * command-line binary. - * - * @param mixed $recipients Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. This may contain recipients not - * specified in the headers, for Bcc:, resending - * messages, etc. - * - * @param array $headers The array of headers to send with the mail, in an - * associative array, where the array key is the - * header name (ie, 'Subject'), and the array value - * is the header value (ie, 'test'). The header - * produced from those values would be 'Subject: - * test'. - * - * @param string $body The full text of the message body, including any - * Mime parts, etc. - * - * @return mixed Returns true on success, or a PEAR_Error - * containing a descriptive error message on - * failure. - * @access public - */ - function send($recipients, $headers, $body) - { - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } - - $result = $this->_sanitizeHeaders($headers); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - $recipients = $this->parseRecipients($recipients); - if (is_a($recipients, 'PEAR_Error')) { - return $recipients; - } - $recipients = escapeShellCmd(implode(' ', $recipients)); - - $headerElements = $this->prepareHeaders($headers); - if (is_a($headerElements, 'PEAR_Error')) { - return $headerElements; - } - list($from, $text_headers) = $headerElements; - - /* Since few MTAs are going to allow this header to be forged - * unless it's in the MAIL FROM: exchange, we'll use - * Return-Path instead of From: if it's set. */ - if (!empty($headers['Return-Path'])) { - $from = $headers['Return-Path']; - } - - if (!isset($from)) { - return PEAR::raiseError('No from address given.'); - } elseif (strpos($from, ' ') !== false || - strpos($from, ';') !== false || - strpos($from, '&') !== false || - strpos($from, '`') !== false) { - return PEAR::raiseError('From address specified with dangerous characters.'); - } - - $from = escapeshellarg($from); // Security bug #16200 - - $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); - if (!$mail) { - return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); - } - - // Write the headers following by two newlines: one to end the headers - // section and a second to separate the headers block from the body. - fputs($mail, $text_headers . $this->sep . $this->sep); - - fputs($mail, $body); - $result = pclose($mail); - if (version_compare(phpversion(), '4.2.3') == -1) { - // With older php versions, we need to shift the pclose - // result to get the exit code. - $result = $result >> 8 & 0xFF; - } - - if ($result != 0) { - return PEAR::raiseError('sendmail returned error code ' . $result, - $result); - } - - return true; - } - -} +<?php +// +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Author: Chuck Hagenbuch <chuck@horde.org> | +// +----------------------------------------------------------------------+ + +/** + * Sendmail implementation of the PEAR Mail:: interface. + * @access public + * @package Mail + * @version $Revision: 294744 $ + */ +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 = implode(' ', array_map('escapeshellarg', $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 index 850695d39fce73df99b98a373358b4214167a086..75171891ea61351ed3ac3530228b549ad4a14a31 100644 --- a/include/pear/Mail/smtp.php +++ b/include/pear/Mail/smtp.php @@ -1,421 +1,453 @@ -<?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.02 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Authors: Chuck Hagenbuch <chuck@horde.org> | -// | Jon Parise <jon@php.net> | -// +----------------------------------------------------------------------+ - -/** Error: Failed to create a Net_SMTP object */ -define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); - -/** Error: Failed to connect to SMTP server */ -define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); - -/** Error: SMTP authentication failure */ -define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); - -/** Error: No From: address has been provided */ -define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); - -/** Error: Failed to set sender */ -define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); - -/** Error: Failed to add recipient */ -define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); - -/** Error: Failed to send data */ -define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); - -/** - * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. - * @access public - * @package Mail - * @version $Revision: 1.33 $ - */ -class Mail_smtp extends Mail { - - /** - * SMTP connection object. - * - * @var object - * @access private - */ - var $_smtp = null; - - /** - * The list of service extension parameters to pass to the Net_SMTP - * mailFrom() command. - * @var array - */ - var $_extparams = array(); - - /** - * The SMTP host to connect to. - * @var string - */ - var $host = 'localhost'; - - /** - * The port the SMTP server is on. - * @var integer - */ - var $port = 25; - - /** - * Should SMTP authentication be used? - * - * This value may be set to true, false or the name of a specific - * authentication method. - * - * If the value is set to true, the Net_SMTP package will attempt to use - * the best authentication method advertised by the remote SMTP server. - * - * @var mixed - */ - var $auth = false; - - /** - * The username to use if the SMTP server requires authentication. - * @var string - */ - var $username = ''; - - /** - * The password to use if the SMTP server requires authentication. - * @var string - */ - var $password = ''; - - /** - * Hostname or domain that will be sent to the remote SMTP server in the - * HELO / EHLO message. - * - * @var string - */ - var $localhost = 'localhost'; - - /** - * SMTP connection timeout value. NULL indicates no timeout. - * - * @var integer - */ - var $timeout = null; - - /** - * Turn on Net_SMTP debugging? - * - * @var boolean $debug - */ - var $debug = false; - - /** - * Indicates whether or not the SMTP connection should persist over - * multiple calls to the send() method. - * - * @var boolean - */ - var $persist = false; - - /** - * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server - * supports it. This speeds up delivery over high-latency connections. By - * default, use the default value supplied by Net_SMTP. - * @var bool - */ - var $pipelining; - - /** - * Constructor. - * - * Instantiates a new Mail_smtp:: object based on the parameters - * passed in. It looks for the following parameters: - * host The server to connect to. Defaults to localhost. - * port The port to connect to. Defaults to 25. - * auth SMTP authentication. Defaults to none. - * username The username to use for SMTP auth. No default. - * password The password to use for SMTP auth. No default. - * localhost The local hostname / domain. Defaults to localhost. - * timeout The SMTP connection timeout. Defaults to none. - * verp Whether to use VERP or not. Defaults to false. - * DEPRECATED as of 1.2.0 (use setMailParams()). - * debug Activate SMTP debug mode? Defaults to false. - * persist Should the SMTP connection persist? - * pipelining Use SMTP command pipelining - * - * If a parameter is present in the $params array, it replaces the - * default. - * - * @param array Hash containing any parameters different from the - * defaults. - * @access public - */ - function Mail_smtp($params) - { - if (isset($params['host'])) $this->host = $params['host']; - if (isset($params['port'])) $this->port = $params['port']; - if (isset($params['auth'])) $this->auth = $params['auth']; - if (isset($params['username'])) $this->username = $params['username']; - if (isset($params['password'])) $this->password = $params['password']; - if (isset($params['localhost'])) $this->localhost = $params['localhost']; - if (isset($params['timeout'])) $this->timeout = $params['timeout']; - if (isset($params['debug'])) $this->debug = (bool)$params['debug']; - if (isset($params['persist'])) $this->persist = (bool)$params['persist']; - if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining']; - - // Deprecated options - if (isset($params['verp'])) { - $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']); - } - - register_shutdown_function(array(&$this, '_Mail_smtp')); - } - - /** - * Destructor implementation to ensure that we disconnect from any - * potentially-alive persistent SMTP connections. - */ - function _Mail_smtp() - { - $this->disconnect(); - } - - /** - * Implements Mail::send() function using SMTP. - * - * @param mixed $recipients Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. This may contain recipients not - * specified in the headers, for Bcc:, resending - * messages, etc. - * - * @param array $headers The array of headers to send with the mail, in an - * associative array, where the array key is the - * header name (e.g., 'Subject'), and the array value - * is the header value (e.g., 'test'). The header - * produced from those values would be 'Subject: - * test'. - * - * @param string $body The full text of the message body, including any - * MIME parts, etc. - * - * @return mixed Returns true on success, or a PEAR_Error - * containing a descriptive error message on - * failure. - * @access public - */ - function send($recipients, $headers, $body) - { - /* If we don't already have an SMTP object, create one. */ - $result = &$this->getSMTPObject(); - if (PEAR::isError($result)) { - return $result; - } - - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } - - $this->_sanitizeHeaders($headers); - - $headerElements = $this->prepareHeaders($headers); - if (is_a($headerElements, 'PEAR_Error')) { - $this->_smtp->rset(); - return $headerElements; - } - list($from, $textHeaders) = $headerElements; - - /* Since few MTAs are going to allow this header to be forged - * unless it's in the MAIL FROM: exchange, we'll use - * Return-Path instead of From: if it's set. */ - if (!empty($headers['Return-Path'])) { - $from = $headers['Return-Path']; - } - - if (!isset($from)) { - $this->_smtp->rset(); - return PEAR::raiseError('No From: address has been provided', - PEAR_MAIL_SMTP_ERROR_FROM); - } - - $params = null; - if (!empty($this->_extparams)) { - foreach ($this->_extparams as $key => $val) { - $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val); - } - } - if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) { - $error = $this->_error("Failed to set sender: $from", $res); - $this->_smtp->rset(); - return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); - } - - $recipients = $this->parseRecipients($recipients); - if (is_a($recipients, 'PEAR_Error')) { - $this->_smtp->rset(); - return $recipients; - } - - foreach ($recipients as $recipient) { - $res = $this->_smtp->rcptTo($recipient); - if (is_a($res, 'PEAR_Error')) { - $error = $this->_error("Failed to add recipient: $recipient", $res); - $this->_smtp->rset(); - return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); - } - } - - /* Send the message's headers and the body as SMTP data. */ - $res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body); - if (is_a($res, 'PEAR_Error')) { - $error = $this->_error('Failed to send data', $res); - $this->_smtp->rset(); - return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); - } - - /* If persistent connections are disabled, destroy our SMTP object. */ - if ($this->persist === false) { - $this->disconnect(); - } - - return true; - } - - /** - * Connection wrapper - * - * - */ - function &connect() { - return $this->getSMTPObject(); - } - - - /** - * Connect to the SMTP server by instantiating a Net_SMTP object. - * - * @return mixed Returns a reference to the Net_SMTP object on success, or - * a PEAR_Error containing a descriptive error message on - * failure. - * - * @since 1.2.0 - * @access public - */ - function &getSMTPObject() - { - if (is_object($this->_smtp) !== false) { - return $this->_smtp; - } - - include_once 'Net/SMTP.php'; - $this->_smtp = &new Net_SMTP($this->host, - $this->port, - $this->localhost); - - /* If we still don't have an SMTP object at this point, fail. */ - if (is_object($this->_smtp) === false) { - return PEAR::raiseError('Failed to create a Net_SMTP object', - PEAR_MAIL_SMTP_ERROR_CREATE); - } - - /* Configure the SMTP connection. */ - if ($this->debug) { - $this->_smtp->setDebug(true); - } - - /* Attempt to connect to the configured SMTP server. */ - if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { - - $error = $this->_error('Failed to connect to ' . - $this->host . ':' . $this->port, - $res); - return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); - } - - /* Attempt to authenticate if authentication has been enabled. */ - if ($this->auth) { - $method = is_string($this->auth) ? $this->auth : ''; - - if (PEAR::isError($res = $this->_smtp->auth($this->username, - $this->password, - $method))) { - $error = $this->_error("$method authentication failure", - $res); - $this->_smtp->rset(); - return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); - } - } - - return $this->_smtp; - } - - /** - * Add parameter associated with a SMTP service extension. - * - * @param string Extension keyword. - * @param string Any value the keyword needs. - * - * @since 1.2.0 - * @access public - */ - function addServiceExtensionParameter($keyword, $value = null) - { - $this->_extparams[$keyword] = $value; - } - - - - - /** - * Disconnect and destroy the current SMTP connection. - * - * @return boolean True if the SMTP connection no longer exists. - * - * @since 1.1.9 - * @access public - */ - function disconnect() - { - /* If we have an SMTP object, disconnect and destroy it. */ - if (is_object($this->_smtp) && $this->_smtp->disconnect()) { - $this->_smtp = null; - } - - /* We are disconnected if we no longer have an SMTP object. */ - return ($this->_smtp === null); - } - - /** - * Build a standardized string describing the current SMTP error. - * - * @param string $text Custom string describing the error context. - * @param object $error Reference to the current PEAR_Error object. - * - * @return string A string describing the current SMTP error. - * - * @since 1.1.7 - * @access private - */ - function _error($text, &$error) - { - /* Split the SMTP response into a code and a response string. */ - list($code, $response) = $this->_smtp->getResponse(); - - /* Build our standardized error string. */ - return $text - . ' [SMTP: ' . $error->getMessage() - . " (code: $code, response: $response)]"; - } - -} +<?php +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010, Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request + * @author Jon Parise <jon@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: smtp.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +/** 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: 294747 $ + */ +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); + list(,$args) = $this->_smtp->getResponse(); + + if (preg_match("/Ok: queued as (.*)/", $args, $queued)) { + $this->queued_as = $queued[1]; + } + + /* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to. + * ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */ + $this->greeting = $this->_smtp->getGreeting(); + + 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; + } + + /** + * 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; + } + + /** + * Connection wrapper + * + * + */ + function &connect() { + return $this->getSMTPObject(); + } + + /** + * 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/Mail/smtpmx.php b/include/pear/Mail/smtpmx.php new file mode 100755 index 0000000000000000000000000000000000000000..f0b69408681bdea1c2d8831616428323b67f8598 --- /dev/null +++ b/include/pear/Mail/smtpmx.php @@ -0,0 +1,502 @@ +<?PHP +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * SMTP MX + * + * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010, gERD Schaufelberger + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_smtpmx + * @author gERD Schaufelberger <gerd@php-tools.net> + * @copyright 2010 gERD Schaufelberger + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: smtpmx.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ + +require_once 'Net/SMTP.php'; + +/** + * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * + * @access public + * @author gERD Schaufelberger <gerd@php-tools.net> + * @package Mail + * @version $Revision: 294747 $ + */ +class Mail_smtpmx extends Mail { + + /** + * SMTP connection object. + * + * @var object + * @access private + */ + var $_smtp = null; + + /** + * The port the SMTP server is on. + * @var integer + * @see getservicebyname() + */ + var $port = 25; + + /** + * Hostname or domain that will be sent to the remote SMTP server in the + * HELO / EHLO message. + * + * @var string + * @see posix_uname() + */ + var $mailname = 'localhost'; + + /** + * SMTP connection timeout value. NULL indicates no timeout. + * + * @var integer + */ + var $timeout = 10; + + /** + * use either PEAR:Net_DNS or getmxrr + * + * @var boolean + */ + var $withNetDns = true; + + /** + * PEAR:Net_DNS_Resolver + * + * @var object + */ + var $resolver; + + /** + * Whether to use VERP or not. If not a boolean, the string value + * will be used as the VERP separators. + * + * @var mixed boolean or string + */ + var $verp = false; + + /** + * Whether to use VRFY or not. + * + * @var boolean $vrfy + */ + var $vrfy = false; + + /** + * Switch to test mode - don't send emails for real + * + * @var boolean $debug + */ + var $test = false; + + /** + * Turn on Net_SMTP debugging? + * + * @var boolean $peardebug + */ + var $debug = false; + + /** + * internal error codes + * + * translate internal error identifier to PEAR-Error codes and human + * readable messages. + * + * @var boolean $debug + * @todo as I need unique error-codes to identify what exactly went wrond + * I did not use intergers as it should be. Instead I added a "namespace" + * for each code. This avoids conflicts with error codes from different + * classes. How can I use unique error codes and stay conform with PEAR? + */ + var $errorCode = array( + 'not_connected' => array( + 'code' => 1, + 'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.' + ), + 'failed_vrfy_rcpt' => array( + 'code' => 2, + 'msg' => 'Recipient "{RCPT}" could not be veryfied.' + ), + 'failed_set_from' => array( + 'code' => 3, + 'msg' => 'Failed to set sender: {FROM}.' + ), + 'failed_set_rcpt' => array( + 'code' => 4, + 'msg' => 'Failed to set recipient: {RCPT}.' + ), + 'failed_send_data' => array( + 'code' => 5, + 'msg' => 'Failed to send mail to: {RCPT}.' + ), + 'no_from' => array( + 'code' => 5, + 'msg' => 'No from address has be provided.' + ), + 'send_data' => array( + 'code' => 7, + 'msg' => 'Failed to create Net_SMTP object.' + ), + 'no_mx' => array( + 'code' => 8, + 'msg' => 'No MX-record for {RCPT} found.' + ), + 'no_resolver' => array( + 'code' => 9, + 'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"' + ), + 'failed_rset' => array( + 'code' => 10, + 'msg' => 'RSET command failed, SMTP-connection corrupt.' + ), + ); + + /** + * Constructor. + * + * Instantiates a new Mail_smtp:: object based on the parameters + * passed in. It looks for the following parameters: + * mailname The name of the local mail system (a valid hostname which matches the reverse lookup) + * port smtp-port - the default comes from getservicebyname() and should work fine + * timeout The SMTP connection timeout. Defaults to 30 seconds. + * vrfy Whether to use VRFY or not. Defaults to false. + * verp Whether to use VERP or not. Defaults to false. + * test Activate test mode? Defaults to false. + * debug Activate SMTP and Net_DNS debug mode? Defaults to false. + * netdns whether to use PEAR:Net_DNS or the PHP build in function getmxrr, default is true + * + * If a parameter is present in the $params array, it replaces the + * default. + * + * @access public + * @param array Hash containing any parameters different from the + * defaults. + * @see _Mail_smtpmx() + */ + function __construct($params) + { + if (isset($params['mailname'])) { + $this->mailname = $params['mailname']; + } else { + // try to find a valid mailname + if (function_exists('posix_uname')) { + $uname = posix_uname(); + $this->mailname = $uname['nodename']; + } + } + + // port number + if (isset($params['port'])) { + $this->_port = $params['port']; + } else { + $this->_port = getservbyname('smtp', 'tcp'); + } + + if (isset($params['timeout'])) $this->timeout = $params['timeout']; + if (isset($params['verp'])) $this->verp = $params['verp']; + if (isset($params['test'])) $this->test = $params['test']; + if (isset($params['peardebug'])) $this->test = $params['peardebug']; + if (isset($params['netdns'])) $this->withNetDns = $params['netdns']; + } + + /** + * Constructor wrapper for PHP4 + * + * @access public + * @param array Hash containing any parameters different from the defaults + * @see __construct() + */ + function Mail_smtpmx($params) + { + $this->__construct($params); + register_shutdown_function(array(&$this, '__destruct')); + } + + /** + * Destructor implementation to ensure that we disconnect from any + * potentially-alive persistent SMTP connections. + */ + function __destruct() + { + if (is_object($this->_smtp)) { + $this->_smtp->disconnect(); + $this->_smtp = null; + } + } + + /** + * Implements Mail::send() function using SMTP direct delivery + * + * @access public + * @param mixed $recipients in RFC822 style or array + * @param array $headers The array of headers to send with the mail. + * @param string $body The full text of the message body, + * @return mixed Returns true on success, or a PEAR_Error + */ + 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; + } + + // Prepare headers + $headerElements = $this->prepareHeaders($headers); + if (is_a($headerElements, 'PEAR_Error')) { + return $headerElements; + } + list($from, $textHeaders) = $headerElements; + + // use 'Return-Path' if possible + if (!empty($headers['Return-Path'])) { + $from = $headers['Return-Path']; + } + if (!isset($from)) { + return $this->_raiseError('no_from'); + } + + // Prepare recipients + $recipients = $this->parseRecipients($recipients); + if (is_a($recipients, 'PEAR_Error')) { + return $recipients; + } + + foreach ($recipients as $rcpt) { + list($user, $host) = explode('@', $rcpt); + + $mx = $this->_getMx($host); + if (is_a($mx, 'PEAR_Error')) { + return $mx; + } + + if (empty($mx)) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('no_mx', $info); + } + + $connected = false; + foreach ($mx as $mserver => $mpriority) { + $this->_smtp = new Net_SMTP($mserver, $this->port, $this->mailname); + + // configure the SMTP connection. + if ($this->debug) { + $this->_smtp->setDebug(true); + } + + // attempt to connect to the configured SMTP server. + $res = $this->_smtp->connect($this->timeout); + if (is_a($res, 'PEAR_Error')) { + $this->_smtp = null; + continue; + } + + // connection established + if ($res) { + $connected = true; + break; + } + } + + if (!$connected) { + $info = array( + 'host' => implode(', ', array_keys($mx)), + 'port' => $this->port, + 'rcpt' => $rcpt, + ); + return $this->_raiseError('not_connected', $info); + } + + // Verify recipient + if ($this->vrfy) { + $res = $this->_smtp->vrfy($rcpt); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_vrfy_rcpt', $info); + } + } + + // mail from: + $args['verp'] = $this->verp; + $res = $this->_smtp->mailFrom($from, $args); + if (is_a($res, 'PEAR_Error')) { + $info = array('from' => $from); + return $this->_raiseError('failed_set_from', $info); + } + + // rcpt to: + $res = $this->_smtp->rcptTo($rcpt); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_set_rcpt', $info); + } + + // Don't send anything in test mode + if ($this->test) { + $result = $this->_smtp->rset(); + $res = $this->_smtp->rset(); + if (is_a($res, 'PEAR_Error')) { + return $this->_raiseError('failed_rset'); + } + + $this->_smtp->disconnect(); + $this->_smtp = null; + return true; + } + + // Send data + $res = $this->_smtp->data("$textHeaders\r\n$body"); + if (is_a($res, 'PEAR_Error')) { + $info = array('rcpt' => $rcpt); + return $this->_raiseError('failed_send_data', $info); + } + + $this->_smtp->disconnect(); + $this->_smtp = null; + } + + return true; + } + + /** + * Recieve mx rexords for a spciefied host + * + * The MX records + * + * @access private + * @param string $host mail host + * @return mixed sorted + */ + function _getMx($host) + { + $mx = array(); + + if ($this->withNetDns) { + $res = $this->_loadNetDns(); + if (is_a($res, 'PEAR_Error')) { + return $res; + } + + $response = $this->resolver->query($host, 'MX'); + if (!$response) { + return false; + } + + foreach ($response->answer as $rr) { + if ($rr->type == 'MX') { + $mx[$rr->exchange] = $rr->preference; + } + } + } else { + $mxHost = array(); + $mxWeight = array(); + + if (!getmxrr($host, $mxHost, $mxWeight)) { + return false; + } + for ($i = 0; $i < count($mxHost); ++$i) { + $mx[$mxHost[$i]] = $mxWeight[$i]; + } + } + + asort($mx); + return $mx; + } + + /** + * initialize PEAR:Net_DNS_Resolver + * + * @access private + * @return boolean true on success + */ + function _loadNetDns() + { + if (is_object($this->resolver)) { + return true; + } + + if (!include_once 'Net/DNS.php') { + return $this->_raiseError('no_resolver'); + } + + $this->resolver = new Net_DNS_Resolver(); + if ($this->debug) { + $this->resolver->test = 1; + } + + return true; + } + + /** + * raise standardized error + * + * include additional information in error message + * + * @access private + * @param string $id maps error ids to codes and message + * @param array $info optional information in associative array + * @see _errorCode + */ + function _raiseError($id, $info = array()) + { + $code = $this->errorCode[$id]['code']; + $msg = $this->errorCode[$id]['msg']; + + // include info to messages + if (!empty($info)) { + $search = array(); + $replace = array(); + + foreach ($info as $key => $value) { + array_push($search, '{' . strtoupper($key) . '}'); + array_push($replace, $value); + } + + $msg = str_replace($search, $replace, $msg); + } + + return PEAR::raiseError($msg, $code); + } + +} diff --git a/include/pear/Net/SMTP.php b/include/pear/Net/SMTP.php index bf0a5a1aa0440a2e6ce1dc0ceb392832e0ab14b8..8d0682f361d85593954d67895e64c2dcb23bea93 100644 --- a/include/pear/Net/SMTP.php +++ b/include/pear/Net/SMTP.php @@ -18,7 +18,7 @@ // | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | // +----------------------------------------------------------------------+ // -// $Id: SMTP.php,v 1.64 2008/12/20 23:03:49 jon Exp $ +// $Id: SMTP.php 314875 2011-08-13 17:03:30Z jon $ require_once 'PEAR.php'; require_once 'Net/Socket.php'; @@ -62,7 +62,7 @@ class Net_SMTP * @var array * @access public */ - var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); + var $auth_methods = array(); /** * Use SMTP command pipelining (specified in RFC 2920) if the SMTP @@ -91,6 +91,13 @@ class Net_SMTP */ var $_debug = false; + /** + * Debug output handler. + * @var callback + * @access private + */ + var $_debug_handler = null; + /** * The socket resource being used to connect to the SMTP server. * @var resource @@ -98,6 +105,21 @@ class Net_SMTP */ var $_socket = null; + /** + * Array of socket options that will be passed to Net_Socket::connect(). + * @see stream_context_create() + * @var array + * @access private + */ + var $_socket_options = null; + + /** + * The socket I/O timeout value in seconds. + * @var int + * @access private + */ + var $_timeout = 0; + /** * The most recent server response code. * @var int @@ -112,6 +134,13 @@ class Net_SMTP */ var $_arguments = array(); + /** + * Stores the SMTP server's greeting string. + * @var string + * @access private + */ + var $_greeting = null; + /** * Stores detected features of the SMTP server. * @var array @@ -134,11 +163,14 @@ class Net_SMTP * @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 + * @param integer $timeout Socket I/O timeout in seconds. + * @param array $socket_options Socket stream_context_create() options. * * @access public * @since 1.0 */ - function Net_SMTP($host = null, $port = null, $localhost = null, $pipelining = false) + function Net_SMTP($host = null, $port = null, $localhost = null, + $pipelining = false, $timeout = 0, $socket_options = null) { if (isset($host)) { $this->host = $host; @@ -152,16 +184,32 @@ class Net_SMTP $this->pipelining = $pipelining; $this->_socket = new Net_Socket(); + $this->_socket_options = $socket_options; + $this->_timeout = $timeout; - /* 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]); + /* Include the Auth_SASL package. If the package is available, we + * enable the authentication methods that depend upon it. */ + if (@include_once 'Auth/SASL.php') { + $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5')); + $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5')); } + + /* These standard authentication methods are always available. */ + $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false); + $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false); + } + + /** + * Set the socket I/O timeout value in seconds plus microseconds. + * + * @param integer $seconds Timeout value in seconds. + * @param integer $microseconds Additional value in microseconds. + * + * @access public + * @since 1.5.0 + */ + function setTimeout($seconds, $microseconds = 0) { + return $this->_socket->setTimeout($seconds, $microseconds); } /** @@ -172,9 +220,30 @@ class Net_SMTP * @access public * @since 1.1.0 */ - function setDebug($debug) + function setDebug($debug, $handler = null) { $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug mesage text. + * + * @access private + * @since 1.3.3 + */ + function _debug($message) + { + if ($this->_debug) { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, + array(&$this, $message)); + } else { + echo "DEBUG: $message\n"; + } + } } /** @@ -182,23 +251,24 @@ class Net_SMTP * * @param string $data The string of data to send. * - * @return mixed True on success or a PEAR_Error object on failure. + * @return mixed The number of bytes that were actually written, + * or a PEAR_Error object on failure. * * @access private * @since 1.1.0 */ function _send($data) { - if ($this->_debug) { - echo "DEBUG: Send: $data\n"; - } + $this->_debug("Send: $data"); - if (PEAR::isError($error = $this->_socket->write($data))) { - return PEAR::raiseError('Failed to write to socket: ' . - $error->getMessage()); + $result = $this->_socket->write($data); + if (!$result || PEAR::isError($result)) { + $msg = ($result) ? $result->getMessage() : "unknown error"; + return PEAR::raiseError("Failed to write to socket: $msg", + null, PEAR_ERROR_RETURN); } - return true; + return $result; } /** @@ -225,7 +295,8 @@ class Net_SMTP } if (strcspn($command, "\r\n") !== strlen($command)) { - return PEAR::raiseError('Commands cannot contain newlines'); + return PEAR::raiseError('Commands cannot contain newlines', + null, PEAR_ERROR_RETURN); } return $this->_send($command . "\r\n"); @@ -255,7 +326,6 @@ class Net_SMTP $this->_code = -1; $this->_arguments = array(); - if ($later) { $this->_pipelined_commands++; return true; @@ -263,14 +333,13 @@ class Net_SMTP for ($i = 0; $i <= $this->_pipelined_commands; $i++) { while ($line = $this->_socket->readLine()) { - if ($this->_debug) { - echo "DEBUG: Recv: $line\n"; - } + $this->_debug("Recv: $line"); - /* If we receive an empty line, the connection has been closed. */ + /* If we receive an empty line, the connection was closed. */ if (empty($line)) { $this->disconnect(); - return PEAR::raiseError('Connection was unexpectedly closed'); + return PEAR::raiseError('Connection was closed', + null, PEAR_ERROR_RETURN); } /* Read the code and store the rest in the arguments array. */ @@ -302,7 +371,32 @@ class Net_SMTP } return PEAR::raiseError('Invalid response code received from server', - $this->_code); + $this->_code, PEAR_ERROR_RETURN); + } + + /** + * Issue an SMTP command and verify its response. + * + * @param string $command The SMTP command string or data. + * @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. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function command($command, $valid) + { + if (PEAR::isError($error = $this->_put($command))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse($valid))) { + return $error; + } + + return true; } /** @@ -320,11 +414,25 @@ class Net_SMTP return array($this->_code, join("\n", $this->_arguments)); } + /** + * Return the SMTP server's greeting string. + * + * @return string A string containing the greeting string, or null if a + * greeting has not been received. + * + * @access public + * @since 1.3.3 + */ + function getGreeting() + { + return $this->_greeting; + } + /** * Attempt to connect to the SMTP server. * * @param int $timeout The timeout value (in seconds) for the - * socket connection. + * socket connection attempt. * @param bool $persistent Should a persistent socket connection * be used? * @@ -335,23 +443,38 @@ class Net_SMTP */ function connect($timeout = null, $persistent = false) { + $this->_greeting = null; $result = $this->_socket->connect($this->host, $this->port, - $persistent, $timeout); - + $persistent, $timeout, + $this->_socket_options); if (PEAR::isError($result)) { return PEAR::raiseError('Failed to connect socket: ' . $result->getMessage()); } + /* + * Now that we're connected, reset the socket's timeout value for + * future I/O operations. This allows us to have different socket + * timeout values for the initial connection (our $timeout parameter) + * and all other socket operations. + */ + if ($this->_timeout > 0) { + if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + return $error; + } + } if (PEAR::isError($error = $this->_parseResponse(220))) { return $error; } + + /* Extract and store a copy of the server's greeting string. */ + list(, $this->_greeting) = $this->getResponse(); + if (PEAR::isError($error = $this->_negotiate())) { return $error; } - return true; } @@ -406,7 +529,8 @@ class Net_SMTP return $error; } if (PEAR::isError($this->_parseResponse(250))) { - return PEAR::raiseError('HELO was not accepted: ', $this->_code); + return PEAR::raiseError('HELO was not accepted: ', $this->_code, + PEAR_ERROR_RETURN); } return true; @@ -440,13 +564,14 @@ class Net_SMTP { $available_methods = explode(' ', $this->_esmtp['AUTH']); - foreach ($this->auth_methods as $method) { + foreach ($this->auth_methods as $method => $callback) { if (in_array($method, $available_methods)) { return $method; } } - return PEAR::raiseError('No supported authentication methods'); + return PEAR::raiseError('No supported authentication methods', + null, PEAR_ERROR_RETURN); } /** @@ -456,15 +581,26 @@ class Net_SMTP * @param string The password to authenticate with. * @param string The requested authentication method. If none is * specified, the best supported method will be used. + * @param bool Flag indicating whether or not TLS should be attempted. + * @param string An optional authorization identifier. If specified, this + * identifier will be used as the authorization proxy. * * @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 = '') + function auth($uid, $pwd , $method = '', $tls = true, $authz = '') { - if (version_compare(PHP_VERSION, '5.1.0', '>=') && (isset($this->_esmtp['STARTTLS']) || ($this->_esmtp['STARTTLS'] == true))) { + /* We can only attempt a TLS connection if one has been requested, + * we're running PHP 5.1.0 or later, have access to the OpenSSL + * extension, are connected to an SMTP server which supports the + * STARTTLS extension, and aren't already connected over a secure + * (SSL) socket connection. */ + if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && + extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && + strncasecmp($this->host, 'ssl://', 6) !== 0) { + /* Start the TLS connection attempt. */ if (PEAR::isError($result = $this->_put('STARTTLS'))) { return $result; } @@ -495,33 +631,27 @@ class Net_SMTP } } else { $method = strtoupper($method); - if (!in_array($method, $this->auth_methods)) { + if (!array_key_exists($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; + if (!isset($this->auth_methods[$method])) { + return PEAR::raiseError("$method is not a supported authentication method"); + } - default: - $result = PEAR::raiseError("$method is not a supported authentication method"); - break; + if (!is_callable($this->auth_methods[$method], false)) { + return PEAR::raiseError("$method authentication method cannot be called"); } + if (is_array($this->auth_methods[$method])) { + list($object, $method) = $this->auth_methods[$method]; + $result = $object->{$method}($uid, $pwd, $authz, $this); + } else { + $func = $this->auth_methods[$method]; + $result = $func($uid, $pwd, $authz, $this); + } + /* If an error was encountered, return the PEAR_Error object. */ if (PEAR::isError($result)) { return $result; @@ -530,18 +660,59 @@ class Net_SMTP return true; } + /** + * Add a new authentication method. + * + * @param string The authentication method name (e.g. 'PLAIN') + * @param mixed The authentication callback (given as the name of a + * function or as an (object, method name) array). + * @param bool Should the new method be prepended to the list of + * available methods? This is the default behavior, + * giving the new method the highest priority. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function setAuthMethod($name, $callback, $prepend = true) + { + if (!is_string($name)) { + return PEAR::raiseError('Method name is not a string'); + } + + if (!is_string($callback) && !is_array($callback)) { + return PEAR::raiseError('Method callback must be string or array'); + } + + if (is_array($callback)) { + if (!is_object($callback[0]) || !is_string($callback[1])) + return PEAR::raiseError('Bad mMethod callback array'); + } + + if ($prepend) { + $this->auth_methods = array_merge(array($name => $callback), + $this->auth_methods); + } else { + $this->auth_methods[$name] = $callback; + } + + return true; + } + /** * Authenticates the user using the DIGEST-MD5 method. * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @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) + function _authDigest_MD5($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { return $error; @@ -558,7 +729,8 @@ class Net_SMTP $challenge = base64_decode($this->_arguments[0]); $digest = &Auth_SASL::factory('digestmd5'); $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, - $this->host, "smtp")); + $this->host, "smtp", + $authz)); if (PEAR::isError($error = $this->_put($auth_str))) { return $error; @@ -585,13 +757,14 @@ class Net_SMTP * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @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) + function _authCRAM_MD5($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { return $error; @@ -624,13 +797,14 @@ class Net_SMTP * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @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) + function _authLogin($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { return $error; @@ -669,13 +843,14 @@ class Net_SMTP * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @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) + function _authPlain($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { return $error; @@ -689,7 +864,7 @@ class Net_SMTP return $error; } - $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd); if (PEAR::isError($error = $this->_put($auth_str))) { return $error; @@ -770,7 +945,7 @@ class Net_SMTP } elseif (trim($params['verp'])) { $args .= ' XVERP=' . $params['verp']; } - } elseif (is_string($params)) { + } elseif (is_string($params) && !empty($params)) { $args .= ' ' . $params; } @@ -842,30 +1017,49 @@ class Net_SMTP /** * Send the DATA command. * - * @param string $data The message body to send. + * @param mixed $data The message data, either as a string or an open + * file resource. + * @param string $headers The message headers. If $headers is provided, + * $data is assumed to contain only body data. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access public * @since 1.0 */ - function data($data) + function data($data, $headers = null) { - /* RFC 1870, section 3, subsection 3 states "a value of zero - * indicates that no fixed maximum message size is in force". - * Furthermore, it says that if "the parameter is omitted no - * information is conveyed about the server's fixed maximum - * message size". */ - if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { - if (strlen($data) >= $this->_esmtp['SIZE']) { - $this->disconnect(); - return PEAR::raiseError('Message size excedes the server limit'); + /* Verify that $data is a supported type. */ + if (!is_string($data) && !is_resource($data)) { + return PEAR::raiseError('Expected a string or file resource'); + } + + /* Start by considering the size of the optional headers string. We + * also account for the addition 4 character "\r\n\r\n" separator + * sequence. */ + $size = (is_null($headers)) ? 0 : strlen($headers) + 4; + + if (is_resource($data)) { + $stat = fstat($data); + if ($stat === false) { + return PEAR::raiseError('Failed to get file size'); } + $size += $stat['size']; + } else { + $size += strlen($data); } - /* Quote the data based on the SMTP standards. */ - $this->quotedata($data); + /* RFC 1870, section 3, subsection 3 states "a value of zero indicates + * that no fixed maximum message size is in force". Furthermore, it + * says that if "the parameter is omitted no information is conveyed + * about the server's fixed maximum message size". */ + $limit = (isset($this->_esmtp['SIZE'])) ? $this->_esmtp['SIZE'] : 0; + if ($limit > 0 && $size >= $limit) { + $this->disconnect(); + return PEAR::raiseError('Message size exceeds server limit'); + } + /* Initiate the DATA command. */ if (PEAR::isError($error = $this->_put('DATA'))) { return $error; } @@ -873,9 +1067,77 @@ class Net_SMTP return $error; } - if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { + /* If we have a separate headers string, send it first. */ + if (!is_null($headers)) { + $this->quotedata($headers); + if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { + return $result; + } + } + + /* Now we can send the message body data. */ + if (is_resource($data)) { + /* Stream the contents of the file resource out over our socket + * connection, line by line. Each line must be run through the + * quoting routine. */ + while (strlen($line = fread($data, 8192)) > 0) { + /* If the last character is an newline, we need to grab the + * next character to check to see if it is a period. */ + while (!feof($data)) { + $char = fread($data, 1); + $line .= $char; + if ($char != "\n") { + break; + } + } + $this->quotedata($line); + if (PEAR::isError($result = $this->_send($line))) { + return $result; + } + } + } else { + /* + * Break up the data by sending one chunk (up to 512k) at a time. + * This approach reduces our peak memory usage. + */ + for ($offset = 0; $offset < $size;) { + $end = $offset + 512000; + + /* + * Ensure we don't read beyond our data size or span multiple + * lines. quotedata() can't properly handle character data + * that's split across two line break boundaries. + */ + if ($end >= $size) { + $end = $size; + } else { + for (; $end < $size; $end++) { + if ($data[$end] != "\n") { + break; + } + } + } + + /* Extract our chunk and run it through the quoting routine. */ + $chunk = substr($data, $offset, $end - $offset); + $this->quotedata($chunk); + + /* If we run into a problem along the way, abort. */ + if (PEAR::isError($result = $this->_send($chunk))) { + return $result; + } + + /* Advance the offset to the end of this chunk. */ + $offset = $end; + } + } + + /* Finally, send the DATA terminator sequence. */ + if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { return $result; } + + /* Verify that the data was successfully received by the server. */ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { return $error; } diff --git a/include/pear/Net/Socket.php b/include/pear/Net/Socket.php index cc495bf9fc621c41c5c30bdef83c06bf1c74670e..8a5ca5f5a0baeb809530d2a51900796a235e6d6a 100644 --- a/include/pear/Net/Socket.php +++ b/include/pear/Net/Socket.php @@ -1,39 +1,51 @@ <?php -// -// +----------------------------------------------------------------------+ -// | PHP Version 4 | -// +----------------------------------------------------------------------+ -// | Copyright (c) 1997-2003 The PHP Group | -// +----------------------------------------------------------------------+ -// | This source file is subject to version 2.0 of the PHP license, | -// | that is bundled with this package in the file LICENSE, and is | -// | available at through the world-wide-web at | -// | http://www.php.net/license/2_02.txt. | -// | If you did not receive a copy of the PHP license and are unable to | -// | obtain it through the world-wide-web, please send a note to | -// | license@php.net so we can mail you a copy immediately. | -// +----------------------------------------------------------------------+ -// | Authors: Stig Bakken <ssb@php.net> | -// | Chuck Hagenbuch <chuck@horde.org> | -// +----------------------------------------------------------------------+ -// -// $Id: Socket.php,v 1.38 2008/02/15 18:24:17 chagenbu Exp $ +/** + * Net_Socket + * + * PHP Version 4 + * + * Copyright (c) 1997-2003 The PHP Group + * + * This source file is subject to version 2.0 of the PHP license, + * that is bundled with this package in the file LICENSE, and is + * available at through the world-wide-web at + * http://www.php.net/license/2_02.txt. + * If you did not receive a copy of the PHP license and are unable to + * obtain it through the world-wide-web, please send a note to + * license@php.net so we can mail you a copy immediately. + * + * Authors: Stig Bakken <ssb@php.net> + * Chuck Hagenbuch <chuck@horde.org> + * + * @category Net + * @package Net_Socket + * @author Stig Bakken <ssb@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @version CVS: $Id: Socket.php 304428 2010-10-15 13:51:46Z clockwerx $ + * @link http://pear.php.net/packages/Net_Socket + */ require_once 'PEAR.php'; -define('NET_SOCKET_READ', 1); +define('NET_SOCKET_READ', 1); define('NET_SOCKET_WRITE', 2); define('NET_SOCKET_ERROR', 4); /** * Generalized Socket class. * - * @version 1.1 - * @author Stig Bakken <ssb@php.net> - * @author Chuck Hagenbuch <chuck@horde.org> + * @category Net + * @package Net_Socket + * @author Stig Bakken <ssb@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @link http://pear.php.net/packages/Net_Socket */ -class Net_Socket extends PEAR { - +class Net_Socket extends PEAR +{ /** * Socket file pointer. * @var resource $fp @@ -78,23 +90,30 @@ class Net_Socket extends PEAR { */ var $lineLength = 2048; + /** + * The string to use as a newline terminator. Usually "\r\n" or "\n". + * @var string $newline + */ + var $newline = "\r\n"; + /** * 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. + * @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) + function connect($addr, $port = 0, $persistent = null, + $timeout = null, $options = null) { if (is_resource($this->fp)) { @fclose($this->fp); @@ -121,9 +140,11 @@ class Net_Socket extends PEAR { } $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen'; - $errno = 0; - $errstr = ''; + $errno = 0; + $errstr = ''; + $old_track_errors = @ini_set('track_errors', 1); + if ($options && function_exists('stream_context_create')) { if ($this->timeout) { $timeout = $this->timeout; @@ -134,29 +155,36 @@ class Net_Socket extends PEAR { // Since PHP 5 fsockopen doesn't allow context specification if (function_exists('stream_socket_client')) { - $flags = $this->persistent ? STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT; + $flags = STREAM_CLIENT_CONNECT; + + if ($this->persistent) { + $flags = STREAM_CLIENT_PERSISTENT; + } + $addr = $this->addr . ':' . $this->port; - $fp = stream_socket_client($addr, $errno, $errstr, $timeout, $flags, $context); + $fp = stream_socket_client($addr, $errno, $errstr, + $timeout, $flags, $context); } else { - $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout, $context); + $fp = @$openfunc($this->addr, $this->port, $errno, + $errstr, $timeout, $context); } } else { if ($this->timeout) { - $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $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)) { + if ($errno == 0 && !strlen($errstr) && 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; @@ -180,6 +208,18 @@ class Net_Socket extends PEAR { return true; } + /** + * Set the newline character/sequence to use. + * + * @param string $newline Newline character(s) + * @return boolean True + */ + function setNewline($newline) + { + $this->newline = $newline; + return true; + } + /** * Find out if the socket is in blocking mode. * @@ -197,7 +237,8 @@ class Net_Socket extends PEAR { * 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. + * @param boolean $mode True for blocking sockets, false for nonblocking. + * * @access public * @return mixed true on success or a PEAR_Error instance otherwise */ @@ -206,9 +247,9 @@ class Net_Socket extends PEAR { if (!is_resource($this->fp)) { return $this->raiseError('not connected'); } - $this->blocking = $mode; - socket_set_blocking($this->fp, $this->blocking); + $this->blocking = $mode; + stream_set_blocking($this->fp, (int)$this->blocking); return true; } @@ -216,8 +257,9 @@ class Net_Socket extends PEAR { * Sets the timeout value on socket descriptor, * expressed in the sum of seconds and microseconds * - * @param integer $seconds Seconds. - * @param integer $microseconds Microseconds. + * @param integer $seconds Seconds. + * @param integer $microseconds Microseconds. + * * @access public * @return mixed true on success or a PEAR_Error instance otherwise */ @@ -234,7 +276,8 @@ class Net_Socket extends PEAR { * Sets the file buffering size on the stream. * See php's stream_set_write_buffer for more information. * - * @param integer $size Write buffer size. + * @param integer $size Write buffer size. + * * @access public * @return mixed on success or an PEAR_Error object otherwise */ @@ -263,7 +306,8 @@ class Net_Socket extends PEAR { * </p> * * @access public - * @return mixed Array containing information about existing socket resource or a PEAR_Error instance otherwise + * @return mixed Array containing information about existing socket + * resource or a PEAR_Error instance otherwise */ function getStatus() { @@ -277,17 +321,23 @@ class Net_Socket extends PEAR { /** * Get a specified line of data * + * @param int $size ?? + * * @access public * @return $size bytes of data from the socket, or a PEAR_Error if * not connected. */ - function gets($size) + function gets($size = null) { if (!is_resource($this->fp)) { return $this->raiseError('not connected'); } - return @fgets($this->fp, $size); + if (is_null($size)) { + return @fgets($this->fp); + } else { + return @fgets($this->fp, $size); + } } /** @@ -296,7 +346,8 @@ class Net_Socket extends PEAR { * 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. + * @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. @@ -313,12 +364,13 @@ class Net_Socket extends PEAR { /** * 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. + * @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 + * @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. */ @@ -335,12 +387,12 @@ class Net_Socket extends PEAR { $blocksize = 1024; } - $pos = 0; + $pos = 0; $size = strlen($data); while ($pos < $size) { $written = @fwrite($this->fp, substr($data, $pos, $blocksize)); - if ($written === false) { - return false; + if (!$written) { + return $written; } $pos += $written; } @@ -350,7 +402,9 @@ class Net_Socket extends PEAR { } /** - * Write a line of data to the socket, followed by a trailing "\r\n". + * Write a line of data to the socket, followed by a trailing newline. + * + * @param string $data Data to write * * @access public * @return mixed fputs result, or an error @@ -361,7 +415,7 @@ class Net_Socket extends PEAR { return $this->raiseError('not connected'); } - return fwrite($this->fp, $data . "\r\n"); + return fwrite($this->fp, $data . $this->newline); } /** @@ -442,7 +496,7 @@ class Net_Socket extends PEAR { } $string = ''; - while (($char = @fread($this->fp, 1)) != "\x00") { + while (($char = @fread($this->fp, 1)) != "\x00") { $string .= $char; } return $string; @@ -482,11 +536,13 @@ class Net_Socket extends PEAR { } $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 rtrim($line, $this->newline); } } return $line; @@ -522,9 +578,9 @@ class Net_Socket extends PEAR { * 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. + * @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 @@ -536,8 +592,8 @@ class Net_Socket extends PEAR { return $this->raiseError('not connected'); } - $read = null; - $write = null; + $read = null; + $write = null; $except = null; if ($state & NET_SOCKET_READ) { $read[] = $this->fp; @@ -548,7 +604,8 @@ class Net_Socket extends PEAR { if ($state & NET_SOCKET_ERROR) { $except[] = $this->fp; } - if (false === ($sr = stream_select($read, $write, $except, $tv_sec, $tv_usec))) { + if (false === ($sr = stream_select($read, $write, $except, + $tv_sec, $tv_usec))) { return false; } @@ -568,15 +625,17 @@ class Net_Socket extends PEAR { /** * 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. + * @param bool $enabled Set this parameter to true to enable encryption + * and false to disable encryption. + * @param integer $type Type of encryption. See stream_socket_enable_crypto() + * for values. * + * @see http://se.php.net/manual/en/function.stream-socket-enable-crypto.php * @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 + * @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) { @@ -586,7 +645,8 @@ class Net_Socket extends PEAR { } return @stream_socket_enable_crypto($this->fp, $enabled, $type); } else { - return $this->raiseError('Net_Socket::enableCrypto() requires php version >= 5.1.0'); + $msg = 'Net_Socket::enableCrypto() requires php version >= 5.1.0'; + return $this->raiseError($msg); } } diff --git a/include/pear/PEAR.php b/include/pear/PEAR.php index 4de89ee3ace62cf172d259cee0733c8b6e6b11d5..2aa85259d62dc69c0cad3f38320bc82fdcf28af9 100644 --- a/include/pear/PEAR.php +++ b/include/pear/PEAR.php @@ -12,9 +12,9 @@ * @author Stig Bakken <ssb@php.net> * @author Tomas V.V.Cox <cox@idecnet.com> * @author Greg Beaver <cellog@php.net> - * @copyright 1997-2009 The Authors + * @copyright 1997-2010 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 $ + * @version CVS: $Id: PEAR.php 313023 2011-07-06 19:17:11Z dufuz $ * @link http://pear.php.net/package/PEAR * @since File available since Release 0.1 */ @@ -78,7 +78,7 @@ $GLOBALS['_PEAR_error_handler_stack'] = array(); * @author Greg Beaver <cellog@php.net> * @copyright 1997-2006 The PHP Group * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version Release: 1.8.1 + * @version Release: 1.9.4 * @link http://pear.php.net/package/PEAR * @see PEAR_Error * @since Class available since PHP 4.0.2 @@ -86,8 +86,6 @@ $GLOBALS['_PEAR_error_handler_stack'] = array(); */ class PEAR { - // {{{ properties - /** * Whether to enable internal debug messages. * @@ -138,10 +136,6 @@ class PEAR */ var $_expected_errors = array(); - // }}} - - // {{{ constructor - /** * Constructor. Registers this object in * $_PEAR_destructor_object_list for destructor emulation if a @@ -158,9 +152,11 @@ class PEAR 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)) { @@ -177,9 +173,6 @@ class PEAR } } - // }}} - // {{{ destructor - /** * Destructor (the emulated type of...). Does nothing right now, * but is included for forward compatibility, so subclass @@ -197,9 +190,6 @@ class PEAR } } - // }}} - // {{{ 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) @@ -226,9 +216,6 @@ class PEAR return $properties[$class][$var]; } - // }}} - // {{{ registerShutdownFunc() - /** * Use this function to register a shutdown method for static * classes. @@ -249,9 +236,6 @@ class PEAR $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); } - // }}} - // {{{ isError() - /** * Tell whether a value is a PEAR error. * @@ -278,9 +262,6 @@ class PEAR 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 @@ -319,7 +300,6 @@ class PEAR * * @since PHP 4.0.5 */ - function setErrorHandling($mode = null, $options = null) { if (isset($this) && is_a($this, 'PEAR')) { @@ -357,9 +337,6 @@ class PEAR } } - // }}} - // {{{ expectError() - /** * This method is used to tell which errors you expect to get. * Expected errors are always returned with error mode @@ -382,12 +359,9 @@ class PEAR } else { array_push($this->_expected_errors, array($code)); } - return sizeof($this->_expected_errors); + return count($this->_expected_errors); } - // }}} - // {{{ popExpect() - /** * This method pops one element off the expected error codes * stack. @@ -399,9 +373,6 @@ class PEAR return array_pop($this->_expected_errors); } - // }}} - // {{{ _checkDelExpect() - /** * This method checks unsets an error code if available * @@ -413,8 +384,7 @@ class PEAR function _checkDelExpect($error_code) { $deleted = false; - - foreach ($this->_expected_errors AS $key => $error_array) { + 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; @@ -425,12 +395,10 @@ class PEAR unset($this->_expected_errors[$key]); } } + return $deleted; } - // }}} - // {{{ delExpect() - /** * This method deletes all occurences of the specified element from * the expected error codes stack. @@ -444,33 +412,26 @@ class PEAR { $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; - } + // $error_code is a non-empty array here; we walk through it trying + // to unset all values + foreach ($error_code as $key => $error) { + $deleted = $this->_checkDelExpect($error) ? true : 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 } + + 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 @@ -525,10 +486,16 @@ class PEAR $message = $message->getMessage(); } - if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ( + isset($this) && + isset($this->_expected_errors) && + count($this->_expected_errors) > 0 && + count($exp = end($this->_expected_errors)) + ) { if ($exp[0] == "*" || (is_int(reset($exp)) && in_array($code, $exp)) || - (is_string(reset($exp)) && in_array($message, $exp))) { + (is_string(reset($exp)) && in_array($message, $exp)) + ) { $mode = PEAR_ERROR_RETURN; } } @@ -569,19 +536,23 @@ class PEAR return $a; } - // }}} - // {{{ throwError() - /** * Simpler form of raiseError with fewer options. In most cases * message, code and userinfo are enough. * - * @param string $message + * @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 string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @access public + * @return object a PEAR error object + * @see PEAR::raiseError */ - function &throwError($message = null, - $code = null, - $userinfo = null) + function &throwError($message = null, $code = null, $userinfo = null) { if (isset($this) && is_a($this, 'PEAR')) { $a = &$this->raiseError($message, $code, null, null, $userinfo); @@ -592,10 +563,9 @@ class PEAR return $a; } - // }}} function staticPushErrorHandling($mode, $options = null) { - $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $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); @@ -664,8 +634,6 @@ class PEAR 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 @@ -699,9 +667,6 @@ class PEAR return true; } - // }}} - // {{{ popErrorHandling() - /** * Pop the last error handler used * @@ -723,9 +688,6 @@ class PEAR return true; } - // }}} - // {{{ loadExtension() - /** * OS independant PHP extension load. Remember to take care * on the correct extension name for case sensitive OSes. @@ -735,39 +697,39 @@ class PEAR */ 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 (extension_loaded($ext)) { + return true; + } - 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'; - } + // if either returns true dl() will produce a FATAL error, stop that + if ( + function_exists('dl') === false || + ini_get('enable_dl') != 1 || + ini_get('safe_mode') == 1 + ) { + return false; + } - return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + 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 true; + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); } - - // }}} } if (PEAR_ZE2) { include_once 'PEAR5.php'; } -// {{{ _PEAR_call_destructors() - function _PEAR_call_destructors() { global $_PEAR_destructor_object_list; @@ -803,14 +765,17 @@ function _PEAR_call_destructors() } // Now call the shutdown functions - if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + if ( + isset($GLOBALS['_PEAR_shutdown_funcs']) && + is_array($GLOBALS['_PEAR_shutdown_funcs']) && + !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 * @@ -823,15 +788,13 @@ function _PEAR_call_destructors() * @author Gregory Beaver <cellog@php.net> * @copyright 1997-2006 The PHP Group * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version Release: 1.8.1 + * @version Release: 1.9.4 * @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; @@ -840,9 +803,6 @@ class PEAR_Error var $userinfo = ''; var $backtrace = null; - // }}} - // {{{ constructor - /** * PEAR_Error constructor * @@ -886,6 +846,7 @@ class PEAR_Error unset($this->backtrace[0]['object']); } } + if ($mode & PEAR_ERROR_CALLBACK) { $this->level = E_USER_NOTICE; $this->callback = $options; @@ -893,20 +854,25 @@ class PEAR_Error 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)) { @@ -919,47 +885,39 @@ class PEAR_Error } 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_CALLBACK && 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() { + 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() { + function getCallback() + { return $this->callback; } - // }}} - // {{{ getMessage() - - /** * Get the error message from an error object. * @@ -971,10 +929,6 @@ class PEAR_Error return ($this->error_message_prefix . $this->message); } - - // }}} - // {{{ getCode() - /** * Get error code from an error object * @@ -986,9 +940,6 @@ class PEAR_Error return $this->code; } - // }}} - // {{{ getType() - /** * Get the name of this error/exception. * @@ -1000,9 +951,6 @@ class PEAR_Error return get_class($this); } - // }}} - // {{{ getUserInfo() - /** * Get additional user-supplied information. * @@ -1014,9 +962,6 @@ class PEAR_Error return $this->userinfo; } - // }}} - // {{{ getDebugInfo() - /** * Get additional debug information supplied by the application. * @@ -1028,9 +973,6 @@ class PEAR_Error return $this->getUserInfo(); } - // }}} - // {{{ getBacktrace() - /** * Get the call backtrace from where the error was generated. * Supported with PHP 4.3.0 or newer. @@ -1050,9 +992,6 @@ class PEAR_Error return $this->backtrace[$frame]; } - // }}} - // {{{ addUserInfo() - function addUserInfo($info) { if (empty($this->userinfo)) { @@ -1062,14 +1001,10 @@ class PEAR_Error } } - // }}} - // {{{ toString() function __toString() { return $this->getMessage(); } - // }}} - // {{{ toString() /** * Make a string representation of this object. @@ -1077,7 +1012,8 @@ class PEAR_Error * @return string a string with an object summary * @access public */ - function toString() { + function toString() + { $modes = array(); $levels = array(E_USER_NOTICE => 'notice', E_USER_WARNING => 'warning', @@ -1116,8 +1052,6 @@ class PEAR_Error $this->error_message_prefix, $this->userinfo); } - - // }}} } /* diff --git a/include/pear/PEAR/FixPHP5PEARWarnings.php b/include/pear/PEAR/FixPHP5PEARWarnings.php index da77106f8ecd3e245f8de483df9a45d7d4755abe..be5dc3ce707c3e06189b89395819ae49edbab19c 100644 --- a/include/pear/PEAR/FixPHP5PEARWarnings.php +++ b/include/pear/PEAR/FixPHP5PEARWarnings.php @@ -4,4 +4,4 @@ if ($skipmsg) { } else { $a = &new $ec($message, $code, $mode, $options, $userinfo); } -?> +?> \ No newline at end of file diff --git a/include/pear/PEAR5.php b/include/pear/PEAR5.php index 5cee090351ff0aac31f0ac628aeec8f69cef01f4..428606780b7bf322bbf8bf2379da80d1340cb86b 100644 --- a/include/pear/PEAR5.php +++ b/include/pear/PEAR5.php @@ -30,4 +30,4 @@ class PEAR5 return $properties[$class][$var]; } -} +} \ No newline at end of file diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php index fc0d418e76606ee7dc64f78fde73db932133ac79..b085329343d101fe69de9b4628ed13ace6d6586b 100644 --- a/include/staff/apikeys.inc.php +++ b/include/staff/apikeys.inc.php @@ -45,9 +45,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="apikeys.php?a=add" class="Icon newapi">Add New API Key</a></b></div> <div class="clear"></div> -<form action="apikeys.php" method="POST" name="keys" onSubmit="return checkbox_checker(this,1,0);"> +<form action="apikeys.php" method="POST" name="keys"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -67,15 +68,13 @@ else if($res && db_num_rows($res)): while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> </td> <td> <?php echo Format::db_date($row['created']); ?></td> <td> <a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td> <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> @@ -90,9 +89,9 @@ else <td colspan="7"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['keys'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['keys'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['keys'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No API keys found'; } ?> @@ -104,16 +103,38 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected API keys?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected API keys?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected API keys?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable"> + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> </form> - +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected API keys? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected API keys? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected API keys?</strong></font> + <br><br>Deleted keys CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/banlist.inc.php b/include/staff/banlist.inc.php index 0b61d1e8b779edaf48c448c82eb38c3d1ce3deed..aaaaaf0b361d9d6aa86e7289ea46a1e9dd5088f2 100644 --- a/include/staff/banlist.inc.php +++ b/include/staff/banlist.inc.php @@ -3,7 +3,7 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$filter) $qstr=''; $select='SELECT rule.* '; -$from='FROM '.EMAIL_FILTER_RULE_TABLE.' rule '; +$from='FROM '.FILTER_RULE_TABLE.' rule '; $where='WHERE rule.filter_id='.db_input($filter->getId()); $search=false; if($_REQUEST['q'] && strlen($_REQUEST['q'])>3) { @@ -17,8 +17,6 @@ if($_REQUEST['q'] && strlen($_REQUEST['q'])>3) { $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'; @@ -71,9 +69,10 @@ if($search) $showing='Search Results: '.$showing; ?> -<form action="banlist.php" method="POST" name="banlist" onSubmit="return checkbox_checker(this,1,0);"> +<form action="banlist.php" method="POST" name="banlist"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -91,15 +90,13 @@ if($search) $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?> - onClick="highLight(this.value,this.checked);"> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?>> + </td> <td> <a href="banlist.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['val']); ?></a></td> <td> <?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> <td><?php echo Format::db_date($row['created']); ?></td> @@ -113,9 +110,9 @@ if($search) <td colspan="5"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['banlist'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['banlist'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['banlist'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No banned emails found!'; } ?> @@ -127,18 +124,41 @@ if($search) if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected email ban?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected emails ban?");'> + <input class="button" type="submit" name="disable" value="Disable" > - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected emails?");'> + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected ban rules? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected ban rules? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected ban rules?</strong></font> + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/cannedresponses.inc.php b/include/staff/cannedresponses.inc.php index a2fe2b02531ce16d735cabee7612e35e10ebc568..c1052be2aa5f6ea2499314dcf033125755cf619f 100644 --- a/include/staff/cannedresponses.inc.php +++ b/include/staff/cannedresponses.inc.php @@ -52,9 +52,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="canned.php?a=add" class="Icon newReply">Add New Response</a></b></div> <div class="clear"></div> -<form action="canned.php" method="POST" name="canned" onSubmit="return checkbox_checker(this,1,0);"> +<form action="canned.php" method="POST" name="canned"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -71,20 +72,16 @@ else $total=0; $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; if($res && db_num_rows($res)): - $defaultId=$cfg->getDefaultDeptId(); while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['canned_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['canned_id'],$ids)) $sel=true; - } $files=$row['files']?'<span class="Icon file"> </span>':''; ?> <tr id="<?php echo $row['canned_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['canned_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> - onClick="highLight(this.value,this.checked);"/> + <input type="checkbox" name="ids[]" value="<?php echo $row['canned_id']; ?>" class="ckb" + <?php echo $sel?'checked="checked"':''; ?> /> </td> <td> <a href="canned.php?id=<?php echo $row['canned_id']; ?>"><?php echo Format::truncate($row['title'],200); echo " $files"; ?></a> @@ -101,9 +98,9 @@ else <td colspan="5"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['canned'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['canned'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['canned'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No canned responses'; } ?> @@ -115,15 +112,41 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected responses?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected responses?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected responses?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable" > + <input class="button" type="submit" name="delete" value="Delete" > </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected canned responses? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected canned responses? + </p> + <p class="confirm-action" style="display:none;" id="mark_overdue-confirm"> + Are you sure want to flag the selected tickets as <font color="red"><b>overdue</b></font>? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected canned responses?</strong></font> + <br><br>Deleted items CANNOT be recovered, including any associated attachments. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php index 8d966d9c2b648645146ed8dff536ad472d20c29c..293388d19485a03f63e71a167e466ac6015ce0a2 100644 --- a/include/staff/categories.inc.php +++ b/include/staff/categories.inc.php @@ -46,9 +46,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="categories.php?a=add" class="Icon newCategory">Add New Category</a></b></div> <div class="clear"></div> -<form action="categories.php" method="POST" name="cat" onSubmit="return checkbox_checker(this,1,0);"> +<form action="categories.php" method="POST" name="cat"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -65,23 +66,20 @@ else $total=0; $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; if($res && db_num_rows($res)): - $defaultId=$cfg->getDefaultDeptId(); while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['category_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['category_id'],$ids)) $sel=true; - } + $faqs=0; if($row['faqs']) $faqs=sprintf('<a href="faq.php?cid=%d">%d</a>',$row['category_id'],$row['faqs']); - ?> <tr id="<?php echo $row['category_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['category_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> - onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" name="ids[]" value="<?php echo $row['category_id']; ?>" class="ckb" + <?php echo $sel?'checked="checked"':''; ?>> + </td> <td><a href="categories.php?id=<?php echo $row['category_id']; ?>"><?php echo Format::truncate($row['name'],200); ?></a> </td> <td><?php echo $row['ispublic']?'<b>Public</b>':'Internal'; ?></td> <td style="text-align:right;padding-right:25px;"><?php echo $faqs; ?></td> @@ -95,9 +93,9 @@ else <td colspan="5"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['cat'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['cat'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['cat'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No FAQ categories found.'; } ?> @@ -109,15 +107,38 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="public" value="Make Public" - onClick=' return confirm("Are you sure you want to make selected categories PUBLIC?");'> - <input class="button" type="submit" name="private" value="Make Internal" - onClick=' return confirm("Are you sure you want to make selected categories INTERNAL?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected categories - including associated FAQs?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="make_public" value="Make Public"> + <input class="button" type="submit" name="make_private" value="Make Internal"> + <input class="button" type="submit" name="delete" value="Delete" > </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="make_public-confirm"> + Are you sure want to make selected categories <b>public</b>? + </p> + <p class="confirm-action" style="display:none;" id="make_private-confirm"> + Are you sure want to make selected categories <b>private</b> (internal)? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected categories?</strong></font> + <br><br>Deleted entries CANNOT be recovered, including any associated FAQs. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php index a46115f422b87abc4446cd4f301e171aa92e539d..64b668463e38ca51b39ba9146b8242493687c175 100644 --- a/include/staff/departments.inc.php +++ b/include/staff/departments.inc.php @@ -46,9 +46,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="departments.php?a=add" class="Icon newDepartment">Add New Department</a></b></div> <div class="clear"></div> -<form action="departments.php" method="POST" name="depts" onSubmit="return checkbox_checker(this,1,0);"> +<form action="departments.php" method="POST" name="depts"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -69,18 +70,17 @@ else $defaultId=$cfg->getDefaultDeptId(); while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['dept_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['dept_id'],$ids)) $sel=true; - } + $row['email']=$row['email_name']?($row['email_name'].' <'.$row['email'].'>'):$row['email']; $default=($defaultId==$row['dept_id'])?' <small>(Default)</small>':''; ?> <tr id="<?php echo $row['dept_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['dept_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> - onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['dept_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> > + </td> <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['dept_name']; ?></a> <?php echo $default; ?></td> <td><?php echo $row['ispublic']?'Public':'<b>Private</b>'; ?></td> <td> @@ -102,9 +102,9 @@ else <td colspan="6"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['depts'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['depts'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['depts'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No department found'; } ?> @@ -115,17 +115,40 @@ else <?php if($res && $num): //Show options.. ?> -<p class="centered"> - <input class="button" type="submit" name="public" value="Make Public" - onClick=' return confirm("Are you sure you want to make selected departments public?");'> - <input class="button" type="submit" name="private" value="Make Private" - onClick=' return confirm("Are you sure you want to make selected departments private?");'> - <input class="button" type="submit" name="delete" value="Delete Dept(s)" - onClick=' return confirm("Are you sure you want to DELETE selected departments?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="make_public" value="Make Public" > + <input class="button" type="submit" name="make_private" value="Make Private" > + <input class="button" type="submit" name="delete" value="Delete Dept(s)" > </p> <?php endif; ?> - </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="make_public-confirm"> + Are you sure want to make selected departments <b>public</b>? + </p> + <p class="confirm-action" style="display:none;" id="make_private-confirm"> + Are you sure want to make selected departments <b>private</b>? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected departments?</strong></font> + <br><br>Deleted departments CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/emails.inc.php b/include/staff/emails.inc.php index 4ab93d22105e3d42ba11efe6321a13d912548ee2..e5f035d1c254656d112879bd4753aaba7ba0908e 100644 --- a/include/staff/emails.inc.php +++ b/include/staff/emails.inc.php @@ -48,9 +48,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="emails.php?a=add" class="Icon newEmail">Add New Email</a></b></div> <div class="clear"></div> -<form action="emails.php" method="POST" name="emails" onSubmit="return checkbox_checker(this,1,0);"> +<form action="emails.php" method="POST" name="emails"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -71,10 +72,8 @@ else $defaultId=$cfg->getDefaultEmailId(); while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['email_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['email_id'],$ids)) $sel=true; - } $default=($row['email_id']==$defaultId); $email=$row['email']; if($row['name']) @@ -82,13 +81,8 @@ else ?> <tr id="<?php echo $row['email_id']; ?>"> <td width=7px> - <?php if($row['email_id']==$defaultId){ ?> - - <?php }else{ ?> - <input type="checkbox" name="ids[]" value="<?php echo $row['email_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> - onClick="highLight(this.value,this.checked);"> - <?php } ?> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['email_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?>> </td> <td><a href="emails.php?id=<?php echo $row['email_id']; ?>"><?php echo Format::htmlchars($email); ?></a> </td> <td><?php echo $row['priority']; ?></td> @@ -104,9 +98,9 @@ else <td colspan="6"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['emails'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['emails'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['emails'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No help emails found'; } ?> @@ -118,12 +112,31 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="delete" value="Delete Email(s)" - onClick=' return confirm("Are you sure you want to DELETE selected emails?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="delete" value="Delete Email(s)" > </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected emails?</strong></font> + <br><br>Deleted emails CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/faq-categories.inc.php b/include/staff/faq-categories.inc.php index eb673a735678e3ebd867f3019b34443822a4b178..11512a03f4a9e2022cbcd314bfe79365056a310c 100644 --- a/include/staff/faq-categories.inc.php +++ b/include/staff/faq-categories.inc.php @@ -61,7 +61,7 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']); if($_REQUEST['q']) - $sql.=" AND MATCH(question,answer,keywords) AGAINST ('".db_input($_REQUEST['q'],false)."')"; + $sql.=" AND question LIKE ('%".db_input($_REQUEST['q'],false)."%') OR answer LIKE ('%".db_input($_REQUEST['q'],false)."%') OR keywords LIKE ('%".db_input($_REQUEST['q'],false)."%')"; $sql.=' GROUP BY faq.faq_id'; echo "<div><strong>Search Results</strong></div><div class='clear'></div>"; diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php index c2aad0bcf4456d5bade4fa323a4ba3c991c39929..36186838bd93cdba6977add500645becba4d673e 100644 --- a/include/staff/filter.inc.php +++ b/include/staff/filter.inc.php @@ -33,7 +33,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr> <th colspan="2"> <h4><?php echo $title; ?></h4> - <em>Filters are executed based on execution order.</em> + <em>Filters are executed based on execution order. Filter can target specific ticket source.</em> </th> </tr> </thead> @@ -71,31 +71,37 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </td> </tr> <tr> - <td width="180"> - To Email Address: + <td width="180" class="required"> + Target: </td> <td> - <select name="email_id"> - <option value="0">— Filter applies to ALL incoming emails ‐</option> - <?php + <select name="target"> + <option value="">— Select a Target ‐</option> + <?php + foreach(Filter::getTargets() as $k => $v) { + echo sprintf('<option value="%s" %s>%s</option>', + $k, (($k==$info['target'])?'selected="selected"':''), $v); + } $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name'; - if(($res=db_query($sql)) && db_num_rows($res)){ - while(list($id,$email,$name)=db_fetch_row($res)){ + if(($res=db_query($sql)) && db_num_rows($res)) { + echo '<OPTGROUP label="Specific System Email">'; + while(list($id,$email,$name)=db_fetch_row($res)) { $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':''; if($name) $email=Format::htmlchars("$name <$email>"); echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email); } + echo '</OPTGROUP>'; } ?> </select> - <br><em>(Highly recommended if the filter is specific to one incoming email address)</em> + + <span class="error">* <?php echo $errors['target']; ?></span> </td> </tr> <tr> <th colspan="2"> - <em><strong>Filter Rules</strong>: Rules are applied based on the criteria. - <span class="error">* <?php echo $errors['rules']; ?></span></em> + <em><strong>Filter Rules</strong>: Rules are applied based on the criteria. <span class="error">* <?php echo $errors['rules']; ?></span></em> </th> </tr> <tr> @@ -156,11 +162,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <tr> <td width="180"> - Ban Email: + Reject Ticket: </td> <td> - <input type="checkbox" name="reject_email" value="1" <?php echo $info['reject_email']?'checked="checked"':''; ?> > - <strong><font class="error">Reject email</font></strong> <em>(All other actions, rules and filters are ignored)</em> + <input type="checkbox" name="reject_ticket" value="1" <?php echo $info['reject_ticket']?'checked="checked"':''; ?> > + <strong><font class="error">Reject Ticket</font></strong> <em>(All other actions and filters are ignored)</em> </td> </tr> <tr> diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php index 7f3aab393f56cb3b1eb3f8dc3da0113204d35e41..106f80586a0fe783546c281da38b88b6ba5327d4 100644 --- a/include/staff/filters.inc.php +++ b/include/staff/filters.inc.php @@ -1,13 +1,13 @@ <?php if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied'); - +$targets = Filter::getTargets(); $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) '. + 'FROM '.FILTER_TABLE.' filter '. + 'LEFT JOIN '.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'); + 'target'=>'filter.target', '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... @@ -28,7 +28,7 @@ $x=$sort.'_sort'; $$x=' class="'.strtolower($order).'" '; $order_by="$order_column $order "; -$total=db_count('SELECT count(*) FROM '.EMAIL_FILTER_TABLE.' filter '); +$total=db_count('SELECT count(*) FROM '.FILTER_TABLE.' filter '); $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; $pageNav=new Pagenate($total, $page, PAGE_LIMIT); $pageNav->setURL('filters.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); @@ -44,23 +44,25 @@ else ?> <div style="width:700;padding-top:5px; float:left;"> - <h2>Email Filters</h2> + <h2>Ticket Filters</h2> </div> <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="filters.php?a=add" class="Icon newEmailFilter">Add New Filter</a></b></div> <div class="clear"></div> -<form action="filters.php" method="POST" name="filters" onSubmit="return checkbox_checker(this,1,0);"> +<form action="filters.php" method="POST" name="filters"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> <tr> <th width="7"> </th> <th width="320"><a <?php echo $name_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=name">Name</a></th> - <th width="100"><a <?php echo $status_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=status">Status</a></th> + <th width="80"><a <?php echo $status_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=status">Status</a></th> <th width="80" style="text-align:center;"><a <?php echo $order_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=order">Order</a></th> <th width="80" style="text-align:center;"><a <?php echo $rules_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=rules">Rules</a></th> + <th width="100"><a <?php echo $target_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=target">Target</a></th> <th width="120" nowrap><a <?php echo $created_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=created">Date Added</a></th> <th width="150" nowrap><a <?php echo $updated_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th> </tr> @@ -72,19 +74,19 @@ else if($res && db_num_rows($res)): while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> + </td> <td> <a href="filters.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a></td> <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> <td style="text-align:right;padding-right:25px;"><?php echo $row['execorder']; ?> </td> <td style="text-align:right;padding-right:25px;"><?php echo $row['rules']; ?> </td> + <td> <?php echo Format::htmlchars($targets[$row['target']]); ?></td> <td> <?php echo Format::db_date($row['created']); ?></td> <td> <?php echo Format::db_datetime($row['updated']); ?></td> </tr> @@ -93,12 +95,12 @@ else endif; ?> <tfoot> <tr> - <td colspan="7"> + <td colspan="8"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['filters'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['filters'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['filters'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No filters found'; } ?> @@ -110,16 +112,40 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected filters?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected filters?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected filters?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable"> + <input class="button" type="submit" name="disable" value="Disable"> + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected filters? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected filters? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected filters?</strong></font> + <br><br>Deleted filters CANNOT be recovered, including any associated rules. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php index a442c3ce02e91c249636d4c762cd9a9ed54b6c89..f2935d72e56237ce35dbc701f563fdeaf475a829 100644 --- a/include/staff/group.inc.php +++ b/include/staff/group.inc.php @@ -134,7 +134,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <tr> <th colspan="2"> - <em><strong>Department Access</strong>: Check all departments the group members are allowed to access. <a href="#" onclick="return select_all(document.forms['group'])">Select All</a> <a href="#" onclick="return reset_all(document.forms['group'])">Select None</a></em> + <em><strong>Department Access</strong>: Check all departments the group members are allowed to access. <a id="selectAll" href="#deptckb">Select All</a> <a id="selectNone" href="#deptckb">Select None</a> </em> </th> </tr> <?php @@ -142,7 +142,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); if(($res=db_query($sql)) && db_num_rows($res)){ while(list($id,$name) = db_fetch_row($res)){ $ck=($info['depts'] && in_array($id,$info['depts']))?'checked="checked"':''; - echo sprintf('<tr><td colspan=2> <input type="checkbox" name="depts[]" value="%d" %s>%s</td></tr>',$id,$ck,$name); + echo sprintf('<tr><td colspan=2> <input type="checkbox" class="deptckb" name="depts[]" value="%d" %s>%s</td></tr>',$id,$ck,$name); } } ?> diff --git a/include/staff/groups.inc.php b/include/staff/groups.inc.php index 2e994710b806bee813dc7daa4d8f89d911efbb3d..64ea1a5b92ef8fff29238587182675a15c83b191 100644 --- a/include/staff/groups.inc.php +++ b/include/staff/groups.inc.php @@ -45,9 +45,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="groups.php?a=add" class="Icon newgroup">Add New Group</a></b></div> <div class="clear"></div> -<form action="groups.php" method="POST" name="groups" onSubmit="return checkbox_checker(this,1,0);"> +<form action="groups.php" method="POST" name="groups"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -68,15 +69,13 @@ else if($res && db_num_rows($res)) { while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['group_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['group_id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['group_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['group_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['group_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> </td> <td><a href="groups.php?id=<?php echo $row['group_id']; ?>"><?php echo $row['group_name']; ?></a> </td> <td> <?php echo $row['group_enabled']?'Active':'<b>Disabled</b>'; ?></td> <td style="text-align:right;padding-right:30px"> @@ -100,9 +99,9 @@ else <td colspan="7"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['groups'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['groups'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['groups'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No groups found!'; } ?> @@ -113,17 +112,40 @@ else <?php if($res && $num): //Show options.. ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected groups?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected groups?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected groups?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable" > + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> - </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected groups? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected groups? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected groups?</strong></font> + <br><br>Deleted groups CANNOT be recovered and might affect staff's access. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index 1f4a3da546c99b645d81c73597cc1c47cc7927c6..a349cb3c6ecb662f2d82f64086fc4984e295c263 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -4,7 +4,7 @@ <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="cache-control" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" /> - <title>osTicket Staff Control Panel</title> + <title><?php echo ($ost && ($title=$ost->getPageTitle()))?$title:'osTicket :: Staff Control Panel'; ?></title> <!--[if IE]> <style type="text/css"> .tip_shadow { display:block !important; } @@ -19,14 +19,17 @@ <script type="text/javascript" src="./js/scp.js"></script> <link rel="stylesheet" href="./css/scp.css" media="screen"> <link rel="stylesheet" href="./css/typeahead.css" media="screen"> - <link type="text/css" href="../css/ui-lightness/jquery-ui-1.8.18.custom.css" rel="stylesheet" /> + <link type="text/css" href="../css/ui-lightness/jquery-ui-1.8.18.custom.css" rel="stylesheet" /> + <link type="text/css" rel="stylesheet" href="../css/font-awesome.css"> + <link type="text/css" rel="stylesheet" href="./css/dropdown.css"> + <script type="text/javascript" src="./js/jquery.dropdown.js"></script> <?php if($ost && ($headers=$ost->getExtraHeaders())) { echo "\n\t".implode("\n\t", $headers)."\n"; } ?> </head> -<body onunload=""> +<body> <div id="container"> <div id="header"> <a href="index.php" id="logo">osTicket - Customer Support System</a> @@ -37,7 +40,7 @@ <?php }else{ ?> | <a href="index.php">Staff Panel</a> <?php } ?> - | <a href="profile.php">My Preferences</a> + | <a href="profile.php">My Preferences</a> | <a href="logout.php?auth=<?php echo md5($ost->getCSRFToken().SECRET_SALT.session_id()); ?>">Log Out</a> </p> </div> @@ -67,8 +70,8 @@ foreach($subnav as $k=> $item) { if($item['droponly']) continue; $class=$item['iconclass']; - if ($activeMenu && $k+1==$activeMenu - or (!$activeMenu + if ($activeMenu && $k+1==$activeMenu + or (!$activeMenu && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false or ($item['urls'] && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls']) diff --git a/include/staff/helptopics.inc.php b/include/staff/helptopics.inc.php index da26fbc850f642004426c075e1986efe625c2e0f..1e15a023c5c964311042d0286b1d34d477944f30 100644 --- a/include/staff/helptopics.inc.php +++ b/include/staff/helptopics.inc.php @@ -53,9 +53,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="helptopics.php?a=add" class="Icon newHelpTopic">Add New Help Topic</a></b></div> <div class="clear"></div> -<form action="helptopics.php" method="POST" name="topics" onSubmit="return checkbox_checker(this,1,0);"> +<form action="helptopics.php" method="POST" name="topics"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -74,19 +75,16 @@ else $total=0; $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; if($res && db_num_rows($res)): - $defaultId=$cfg->getDefaultDeptId(); while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['topic_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['topic_id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['topic_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['topic_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> - onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['topic_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> + </td> <td><a href="helptopics.php?id=<?php echo $row['topic_id']; ?>"><?php echo $row['name']; ?></a> </td> <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> <td><?php echo $row['ispublic']?'Public':'<b>Private</b>'; ?></td> @@ -102,9 +100,9 @@ else <td colspan="7"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['topics'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['topics'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['topics'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No help topics found'; } ?> @@ -116,17 +114,40 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected help topics?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected help topics?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected help topics?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable"> + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> - </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected help topics? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected help topics? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected help topics?</strong></font> + <br><br>Deleted topics CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php index 2d8a41f650601c2ec8396ecb89a8ea5ac90a4790..b8b136eb56d01282e8b15dbd4c0f11085e437eaa 100644 --- a/include/staff/login.tpl.php +++ b/include/staff/login.tpl.php @@ -1,4 +1,8 @@ -<?php defined('OSTSCPINC') or die('Invalid path'); ?> +<?php +defined('OSTSCPINC') or die('Invalid path'); + +$info = ($_POST && $errors)?Format::htmlchars($_POST):array(); +?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> @@ -9,6 +13,12 @@ <meta http-equiv="cache-control" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> + <script type="text/javascript" src="../js/jquery-1.7.2.min.js"></script> + <script type="text/javascript"> + $(document).ready(function() { + $("input:not(.dp):visible:enabled:first").focus(); + }); + </script> </head> <body id="loginBody"> <div id="loginBox"> @@ -16,9 +26,9 @@ <h3><?php echo Format::htmlchars($msg); ?></h3> <form action="login.php" method="post"> <?php csrf_token(); ?> - <input type="hidden" name="d"o value="scplogin"> + <input type="hidden" name="do" value="scplogin"> <fieldset> - <input type="text" name="username" id="name" value="" placeholder="username" autocorrect="off" autocapitalize="off"> + <input type="text" name="username" id="name" value="<?php echo $info['username']; ?>" placeholder="username" autocorrect="off" autocapitalize="off"> <input type="password" name="passwd" id="pass" placeholder="password" autocorrect="off" autocapitalize="off"> </fieldset> <input class="submit" type="submit" name="submit" value="Log In"> diff --git a/include/staff/settings-alerts.inc.php b/include/staff/settings-alerts.inc.php index 5bb5d393246ede7266440c30a7e9f5e41df8cc8e..cc91ffdf804fce3dad988a6e11139a1b259d60c8 100644 --- a/include/staff/settings-alerts.inc.php +++ b/include/staff/settings-alerts.inc.php @@ -1,102 +1,177 @@ +<h2>Alerts and Notices</h2> <form action="settings.php?t=alerts" method="post" id="save"> <?php csrf_token(); ?> <input type="hidden" name="t" value="alerts" > <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> - <th colspan="2"> - <h4>Alerts and Notices Setting</h4> - <em>Alerts sent to staff on ticket "events". Staff assignment takes precedence over team assignment.</em> + <th> + <h4>Alerts and Notices sent to staff on ticket "events"</h4> </th> </tr> </thead> <tbody> + <tr><th><em><b>New Ticket Alert</b>: Alert sent out on new tickets</em></th></tr> <tr> - <td width="160">New Ticket Alert:</td> - <td> + <td><em><b>Status:</b></em> <input type="radio" name="ticket_alert_active" value="1" <?php echo $config['ticket_alert_active']?'checked':''; ?> />Enable <input type="radio" name="ticket_alert_active" value="0" <?php echo !$config['ticket_alert_active']?'checked':''; ?> />Disable - <em>Alert sent out on new tickets <font class="error"> <?php echo $errors['ticket_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: - <input type="checkbox" name="ticket_alert_admin" <?php echo $config['ticket_alert_admin']?'checked':''; ?>> Admin Email + <font class="error"> <?php echo $errors['ticket_alert_active']; ?></font></em> + </td> + </tr> + <tr> + <td> + <input type="checkbox" name="ticket_alert_admin" <?php echo $config['ticket_alert_admin']?'checked':''; ?>> Admin Email <em>(<?php echo $cfg->getAdminEmail(); ?>)</em> + </td> + </tr> + <tr> + <td> <input type="checkbox" name="ticket_alert_dept_manager" <?php echo $config['ticket_alert_dept_manager']?'checked':''; ?>> Department Manager - <input type="checkbox" name="ticket_alert_dept_members" <?php echo $config['ticket_alert_dept_members']?'checked':''; ?>> Department Members (spammy) </td> </tr> <tr> - <td width="160">New Message Alert:</td> <td> + <input type="checkbox" name="ticket_alert_dept_members" <?php echo $config['ticket_alert_dept_members']?'checked':''; ?>> Department Members <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>New Message Alert</b>: Alert sent out when a new message, from the user, is appended to an existing ticket</em></th></tr> + <tr> + <td><em><b>Status:</b></em> <input type="radio" name="message_alert_active" value="1" <?php echo $config['message_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="message_alert_active" value="0" <?php echo !$config['message_alert_active']?'checked':''; ?> />Disable - <em>Alert sent out when a new message is appended to an existing ticket <font class="error"> <?php echo $errors['message_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: + </td> + </tr> + <tr> + <td> <input type="checkbox" name="message_alert_laststaff" <?php echo $config['message_alert_laststaff']?'checked':''; ?>> Last Respondent + </td> + </tr> + <tr> + <td> <input type="checkbox" name="message_alert_assigned" <?php echo $config['message_alert_assigned']?'checked':''; ?>> Assigned Staff - <input type="checkbox" name="message_alert_dept_manager" <?php echo $config['message_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) </td> </tr> <tr> - <td width="160">New Internal Note Alert:</td> <td> + <input type="checkbox" name="message_alert_dept_manager" <?php echo $config['message_alert_dept_manager']?'checked':''; ?>> Department Manager <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>New Internal Note Alert</b>: Alert sent out when a new internal note is posted.</em></th></tr> + <tr> + <td><em><b>Status:</b></em> <input type="radio" name="note_alert_active" value="1" <?php echo $config['note_alert_active']?'checked':''; ?> />Enable + <input type="radio" name="note_alert_active" value="0" <?php echo !$config['note_alert_active']?'checked':''; ?> />Disable - <em>Alert sent out when a new internal note is posted <font class="error"> <?php echo $errors['note_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: + <font class="error"> <?php echo $errors['note_alert_active']; ?></font> + </td> + </tr> + <tr> + <td> <input type="checkbox" name="note_alert_laststaff" <?php echo $config['note_alert_laststaff']?'checked':''; ?>> Last Respondent + </td> + </tr> + <tr> + <td> <input type="checkbox" name="note_alert_assigned" <?php echo $config['note_alert_assigned']?'checked':''; ?>> Assigned Staff - <input type="checkbox" name="note_alert_dept_manager" <?php echo $config['note_alert_dept_manager']?'checked':''; ?>> Department Manager (spammy) </td> </tr> <tr> - <td width="160">Ticket Assignment Alert:</td> <td> + <input type="checkbox" name="note_alert_dept_manager" <?php echo $config['note_alert_dept_manager']?'checked':''; ?>> Department Manager <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>Ticket Assignment Alert</b>: Alert sent out to staff on ticket assignment.</em></th></tr> + <tr> + <td><em><b>Status: </b></em> <input name="assigned_alert_active" value="1" checked="checked" type="radio">Enable + <input name="assigned_alert_active" value="0" type="radio">Disable - <em>Alert sent out to staff on ticket assignment <font class="error"> <?php echo $errors['assigned_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: + <font class="error"> <?php echo $errors['assigned_alert_active']; ?></font> + </td> + </tr> + <tr> + <td> <input type="checkbox" name="assigned_alert_staff" <?php echo $config['assigned_alert_staff']?'checked':''; ?>> Assigned Staff - <input type="checkbox"name="assigned_alert_team_lead" <?php echo $config['assigned_alert_team_lead']?'checked':''; ?>>Team Lead (Team assignment) - <input type="checkbox"name="assigned_alert_team_members" <?php echo $config['assigned_alert_team_members']?'checked':''; ?>> - Team Members (spammy) </td> </tr> <tr> - <td width="160">Ticket Transfer Alert:</td> <td> + <input type="checkbox"name="assigned_alert_team_lead" <?php echo $config['assigned_alert_team_lead']?'checked':''; ?>>Team Lead <em>(On team assignment)</em> + </td> + </tr> + <tr> + <td> + <input type="checkbox"name="assigned_alert_team_members" <?php echo $config['assigned_alert_team_members']?'checked':''; ?>> + Team Members <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>Ticket Transfer Alert</b>: Alert sent out to staff of the target department on ticket transfer.</em></th></tr> + <tr> + <td><em><b>Status:</b></em> <input type="radio" name="transfer_alert_active" value="1" <?php echo $config['transfer_alert_active']?'checked':''; ?> />Enable <input type="radio" name="transfer_alert_active" value="0" <?php echo !$config['transfer_alert_active']?'checked':''; ?> />Disable - <em>Alert sent out to staff on ticket transfer <font class="error"> -<?php echo $errors['alert_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: + <font class="error"> <?php echo $errors['alert_alert_active']; ?></font> + </td> + </tr> + <tr> + <td> <input type="checkbox" name="transfer_alert_assigned" <?php echo $config['transfer_alert_assigned']?'checked':''; ?>> Assigned Staff/Team + </td> + </tr> + <tr> + <td> <input type="checkbox" name="transfer_alert_dept_manager" <?php echo $config['transfer_alert_dept_manager']?'checked':''; ?>> Department Manager - <input type="checkbox" name="transfer_alert_dept_members" <?php echo $config['transfer_alert_dept_members']?'checked':''; ?>> Department Members - (spammy) </td> </tr> <tr> - <td width="160">Overdue Ticket Alert:</td> <td> + <input type="checkbox" name="transfer_alert_dept_members" <?php echo $config['transfer_alert_dept_members']?'checked':''; ?>> + Department Members <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>Overdue Ticket Alert</b>: Alert sent out when a ticket becomes overdue - admin email gets an alert by default.</em></th></tr> + <tr> + <td><em><b>Status:</b></em> <input type="radio" name="overdue_alert_active" value="1" <?php echo $config['overdue_alert_active']?'checked':''; ?> />Enable <input type="radio" name="overdue_alert_active" value="0" <?php echo !$config['overdue_alert_active']?'checked':''; ?> />Disable - <em>Alert sent out when a ticket becomes overdue - admin email gets an alert by default. <font class="error"> <?php echo $errors['overdue_alert_active']; ?></font></em><br> - <strong>Recipients</strong>: + <font class="error"> <?php echo $errors['overdue_alert_active']; ?></font> + </td> + </tr> + <tr> + <td> <input type="checkbox" name="overdue_alert_assigned" <?php echo $config['overdue_alert_assigned']?'checked':''; ?>> Assigned Staff/Team + </td> + </tr> + <tr> + <td> <input type="checkbox" name="overdue_alert_dept_manager" <?php echo $config['overdue_alert_dept_manager']?'checked':''; ?>> Department Manager - <input type="checkbox" name="overdue_alert_dept_members" <?php echo $config['overdue_alert_dept_members']?'checked':''; ?>> Department Members (spammy) </td> </tr> <tr> - <td width="160">System Alerts:</td> - <td><em><b>Enabled</b>: Errors are sent to system admin email (<?php echo $cfg->getAdminEmail(); ?>)</em><br> + <td> + <input type="checkbox" name="overdue_alert_dept_members" <?php echo $config['overdue_alert_dept_members']?'checked':''; ?>> Department Members <em>(spammy)</em> + </td> + </tr> + <tr><th><em><b>System Alerts</b>: Enabled by default. Errors are sent to system admin email (<?php echo $cfg->getAdminEmail(); ?>)</em></th></tr> + <tr> + <td> <input type="checkbox" name="send_sys_errors" checked="checked" disabled="disabled">System Errors + </td> + </tr> + <tr> + <td> <input type="checkbox" name="send_sql_errors" <?php echo $config['send_sql_errors']?'checked':''; ?>>SQL errors + </td> + </tr> + <tr> + <td> <input type="checkbox" name="send_login_errors" <?php echo $config['send_login_errors']?'checked':''; ?>>Excessive Login attempts </td> </tr> </tbody> </table> -<p style="padding-left:200px;"> +<p style="padding-left:350px;"> <input class="button" type="submit" name="submit" value="Save Changes"> <input class="button" type="reset" name="reset" value="Reset Changes"> </p> diff --git a/include/staff/settings-attachments.inc.php b/include/staff/settings-attachments.inc.php deleted file mode 100644 index b381fa40d334f9f2c9077f91698c6ec90aa31491..0000000000000000000000000000000000000000 --- a/include/staff/settings-attachments.inc.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php -if(!($maxfileuploads=ini_get('max_file_uploads'))) - $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; - -?> -<form action="settings.php?t=attachments" method="post" id="save"> -<?php csrf_token(); ?> -<input type="hidden" name="t" value="attachments" > -<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> - <thead> - <tr> - <th colspan="2"> - <h4>Attachments Settings</h4> - <em> Before enabling attachments make sure you understand PHP file upload settings and security issues related to file upload.</em> - </th> - </tr> - </thead> - <tbody> - <tr> - <td width="180">Allow Attachments:</td> - <td> - <input type="checkbox" name="allow_attachments" <?php echo $config['allow_attachments']?'checked="checked"':''; ?>><b>Allow Attachments</b> - <em>(Global Setting)</em> - <font class="error"> <?php echo $errors['allow_attachments']; ?></font> - </td> - </tr> - <tr> - <td width="180">Emailed Attachments:</td> - <td> - <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed files - <font class="error"> <?php echo $errors['allow_email_attachments']; ?></font> - </td> - </tr> - <tr> - <td width="180">Online Attachments:</td> - <td> - <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> > - Allow web upload - <input type="checkbox" name="allow_online_attachments_onlogin" <?php echo $config['allow_online_attachments_onlogin'] ?'checked="checked"':''; ?> > - Limit to authenticated users only. <em>(User must be logged in to upload files)</em> - <font class="error"> <?php echo $errors['allow_online_attachments']; ?></font> - </td> - </tr> - <tr> - <td>Max. User File Uploads:</td> - <td> - <select name="max_user_file_uploads"> - <?php - for($i = 1; $i <=$maxfileuploads; $i++) { - ?> - <option <?php echo $config['max_user_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> - <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> - <?php - } ?> - </select> - <em>(Number of files the user is allowed to upload simultaneously)</em> - <font class="error"> <?php echo $errors['max_user_file_uploads']; ?></font> - </td> - </tr> - <tr> - <td>Max. Staff File Uploads:</td> - <td> - <select name="max_staff_file_uploads"> - <?php - for($i = 1; $i <=$maxfileuploads; $i++) { - ?> - <option <?php echo $config['max_staff_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> - <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> - <?php - } ?> - </select> - <em>(Number of files the staff is allowed to upload simultaneously)</em> - <font class="error"> <?php echo $errors['max_staff_file_uploads']; ?></font> - </td> - </tr> - <tr> - <td width="180">Maximum File Size:</td> - <td> - <input type="text" name="max_file_size" value="<?php echo $config['max_file_size']; ?>"> in bytes. - <em>(Max <?php echo Format::file_size(ini_get('upload_max_filesize')); ?>)</em> - <font class="error"> <?php echo $errors['max_file_size']; ?></font> - </td> - </tr> - <tr> - <td width="180">Ticket Response Files:</td> - <td> - <input type="checkbox" name="email_attachments" <?php echo $config['email_attachments']?'checked="checked"':''; ?> >Email attachments to the user - </td> - </tr> - <tr> - <th colspan="2"> - <em><strong>Accepted File Types</strong>: Limit the type of files users are allowed to upload. - <font class="error"> <?php echo $errors['allowed_filetypes']; ?></font></em> - </th> - </tr> - <tr> - <td colspan="2"> - <em>Enter allowed file extensions separated by a comma. e.g .doc, .pdf. To accept all files enter wildcard <b><i>.*</i></b> i.e dotStar (NOT Recommended).</em><br> - <textarea name="allowed_filetypes" cols="21" rows="4" style="width: 65%;" wrap="hard" ><?php echo $config['allowed_filetypes']; ?></textarea> - </td> - </tr> - </tbody> -</table> -<p style="padding-left:210px;"> - <input class="button" type="submit" name="submit" value="Save Changes"> - <input class="button" type="reset" name="reset" value="Reset Changes"> -</p> -</form> diff --git a/include/staff/settings-autoresponders.inc.php b/include/staff/settings-autoresp.inc.php similarity index 93% rename from include/staff/settings-autoresponders.inc.php rename to include/staff/settings-autoresp.inc.php index 106e7f3f4d7063a6bcb4835cdbd07006b6f16b5c..b5815915fc346bb97fbfa318db9416bda6bfccce 100644 --- a/include/staff/settings-autoresponders.inc.php +++ b/include/staff/settings-autoresp.inc.php @@ -1,6 +1,7 @@ -<form action="settings.php?t=autoresponders" method="post" id="save"> +<h2>Autoresponder Settings</h2> +<form action="settings.php?t=autoresp" method="post" id="save"> <?php csrf_token(); ?> -<input type="hidden" name="t" value="autoresponders" > +<input type="hidden" name="t" value="autoresp" > <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> diff --git a/include/staff/settings-dates.inc.php b/include/staff/settings-dates.inc.php deleted file mode 100644 index f8085cfc3193342bf1bd6c66df87b48d9dcf5edd..0000000000000000000000000000000000000000 --- a/include/staff/settings-dates.inc.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -$gmtime=Misc::gmtime(); -?> -<form action="settings.php?t=dates" method="post" id="save"> -<?php csrf_token(); ?> -<input type="hidden" name="t" value="dates" > -<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> - <thead> - <tr> - <th colspan="2"> - <h4>Date and Time Options</h4> - <em>Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em> - </th> - </tr> - </thead> - <tbody> - <tr><td width="220" class="required">Time Format:</td> - <td> - <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>"> - <font class="error">* <?php echo $errors['time_format']; ?></font> - <em><?php echo Format::date($config['time_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em></td> - </tr> - <tr><td width="220" class="required">Date Format:</td> - <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>"> - <font class="error">* <?php echo $errors['date_format']; ?></font> - <em><?php echo Format::date($config['date_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> - </td> - </tr> - <tr><td width="220" class="required">Date & Time Format:</td> - <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>"> - <font class="error">* <?php echo $errors['datetime_format']; ?></font> - <em><?php echo Format::date($config['datetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> - </td> - </tr> - <tr><td width="220" class="required">Day, Date & Time Format:</td> - <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>"> - <font class="error">* <?php echo $errors['daydatetime_format']; ?></font> - <em><?php echo Format::date($config['daydatetime_format'],$gmtime,$config['timezone_offset'],$config['enable_daylight_saving']); ?></em> - </td> - </tr> - <tr><td width="220" class="required">Default Time Zone:</td> - <td> - <select name="default_timezone_id"> - <option value="">— Select Default Time Zone —</option> - <?php - $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; - if(($res=db_query($sql)) && db_num_rows($res)){ - while(list($id,$offset, $tz)=db_fetch_row($res)){ - $sel=($config['default_timezone_id']==$id)?'selected="selected"':''; - echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); - } - } - ?> - </select> - <font class="error">* <?php echo $errors['default_timezone_id']; ?></font> - </td> - </tr> - <tr> - <td width="220">Daylight Saving:</td> - <td> - <input type="checkbox" name="enable_daylight_saving" <?php echo $config['enable_daylight_saving'] ? 'checked="checked"': ''; ?>>Observe daylight savings - </td> - </tr> - </tbody> -</table> -<p style="padding-left:250px;"> - <input class="button" type="submit" name="submit" value="Save Changes"> - <input class="button" type="reset" name="reset" value="Reset Changes"> -</p> -</form> diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php index e4ccaf3a4e69329c15d474bfd3f136f9a8fe17f9..f8674095d49cfe25102ef682e64bb9df59e1614a 100644 --- a/include/staff/settings-emails.inc.php +++ b/include/staff/settings-emails.inc.php @@ -1,110 +1,118 @@ -<form action="settings.php?t=emails" method="post" id="save"> +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); +?> +<h2>Email Settings and Options</h2> +<form action="settings.php?t=emails" method="post" id="save"> <?php csrf_token(); ?> -<input type="hidden" name="t" value="emails" > -<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> - <thead> - <tr> - <th colspan="2"> - <h4>Email Settings</h4> - <em>Note that some of the global settings can be overwritten at department/email level.</em> - </th> - </tr> - </thead> - <tbody> - <tr> - <td width="180" class="required">Default System Email:</td> - <td> - <select name="default_email_id"> - <option value=0 disabled>Select One</option> +<input type="hidden" name="t" value="emails" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Email Settings</h4> + <em>Note that some of the global settings can be overwritten at department/email level.</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Default System Email:</td> + <td> + <select name="default_email_id"> + <option value=0 disabled>Select One</option> <?php - $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE; - if(($res=db_query($sql)) && db_num_rows($res)){ - while (list($id,$email,$name) = db_fetch_row($res)){ - $email=$name?"$name <$email>":$email; - ?> - <option value="<?php echo $id; ?>"<?php echo ($config['default_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE; + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$email,$name) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> <?php - } - } ?> - </select> - <font class="error">* <?php echo $errors['default_email_id']; ?></font> - </td> - </tr> - <tr> - <td width="180" class="required">Default Alert Email:</td> - <td> - <select name="alert_email_id"> - <option value="0" selected="selected">Use Default System Email (above)</option> + } + } ?> + </select> + <font class="error">* <?php echo $errors['default_email_id']; ?></font> + </td> + </tr> + <tr> + <td width="180" class="required">Default Alert Email:</td> + <td> + <select name="alert_email_id"> + <option value="0" selected="selected">Use Default System Email (above)</option> <?php - $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' WHERE email_id != '.db_input($config['default_email_id']); - if(($res=db_query($sql)) && db_num_rows($res)){ - while (list($id,$email,$name) = db_fetch_row($res)){ - $email=$name?"$name <$email>":$email; - ?> - <option value="<?php echo $id; ?>"<?php echo ($config['alert_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' WHERE email_id != '.db_input($config['default_email_id']); + if(($res=db_query($sql)) && db_num_rows($res)){ + while (list($id,$email,$name) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['alert_email_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> <?php - } - } ?> - </select> - <font class="error">* <?php echo $errors['alert_email_id']; ?></font> - </td> - </tr> - <tr> - <td width="180" class="required">Admin Email Address:</td> - <td> - <input type="text" size=40 name="admin_email" value="<?php echo $config['admin_email']; ?>"> - <font class="error">* <?php echo $errors['admin_email']; ?></font></td> - </tr> - <tr><th colspan=2><em><strong>Incoming Emails</strong>: For mail fetcher (POP/IMAP) to work you must set a cron job or enable auto-cron</em></th> - <tr> - <td width="180">Email Polling:</td> - <td><input type="checkbox" name="enable_mail_polling" value=1 <?php echo $config['enable_mail_polling']? 'checked="checked"': ''; ?> > Enable POP/IMAP - <em>(Global setting which can be disabled at email level)</em> - </td> - </tr> - <tr> - <td width="180">Email Piping:</td> - <td><input type="checkbox" name="enable_email_piping" value=1 <?php echo $config['enable_email_piping']? 'checked="checked"': ''; ?>> Enable email piping - <em>(You pipe we accept policy)</em> - </td> - </tr> - <tr> - <td width="180">Strip Quoted Reply:</td> - <td> - <input type="checkbox" name="strip_quoted_reply" <?php echo $config['strip_quoted_reply'] ? 'checked="checked"':''; ?>> - <em>(depends on the reply separator tag set below)</em> - <font class="error"> <?php echo $errors['strip_quoted_reply']; ?></font> - </td> - </tr> - <tr> - <td width="180">Reply Separator Tag:</td> - <td><input type="text" name="reply_separator" value="<?php echo $config['reply_separator']; ?>"> - <font class="error"> <?php echo $errors['reply_separator']; ?></font> - </td> - </tr> - <tr><th colspan=2><em><strong>Outgoing Emails</strong>: Default email only applies to outgoing emails without SMTP setting.</em></th></tr> - <tr><td width="180">Default Outgoing Email:</td> - <td> - <select name="default_smtp_id"> - <option value=0 selected="selected">None: Use PHP mail function</option> + } + } ?> + </select> + <font class="error">* <?php echo $errors['alert_email_id']; ?></font> + </td> + </tr> + <tr> + <td width="180" class="required">Admin's Email Address:</td> + <td> + <input type="text" size=40 name="admin_email" value="<?php echo $config['admin_email']; ?>"> + <font class="error">* <?php echo $errors['admin_email']; ?></font> + <em>(System administrator's email)</em> + </td> + </tr> + <tr><th colspan=2><em><strong>Incoming Emails</strong>: For mail fetcher (polling) to work you must set an external cron job or enable auto-cron</em></th> + <tr> + <td width="180">Email Polling:</td> + <td><input type="checkbox" name="enable_mail_polling" value=1 <?php echo $config['enable_mail_polling']? 'checked="checked"': ''; ?> > Enable POP/IMAP polling + + <input type="checkbox" name="enable_auto_cron" <?php echo $config['enable_auto_cron']?'checked="checked"':''; ?>> + Enable Auto-Cron <em>(Poll based on staff activity - NOT recommended)</em> + </td> + </tr> + <tr> + <td width="180">Email Piping:</td> + <td><input type="checkbox" name="enable_email_piping" value=1 <?php echo $config['enable_email_piping']? 'checked="checked"': ''; ?>> Enable email piping + <em>(You pipe we accept policy)</em> + </td> + </tr> + <tr> + <td width="180">Strip Quoted Reply:</td> + <td> + <input type="checkbox" name="strip_quoted_reply" <?php echo $config['strip_quoted_reply'] ? 'checked="checked"':''; ?>> + <em>(depends on the reply separator tag set below)</em> + <font class="error"> <?php echo $errors['strip_quoted_reply']; ?></font> + </td> + </tr> + <tr> + <td width="180">Reply Separator Tag:</td> + <td><input type="text" name="reply_separator" value="<?php echo $config['reply_separator']; ?>"> + <font class="error"> <?php echo $errors['reply_separator']; ?></font> + </td> + </tr> + <tr><th colspan=2><em><strong>Outgoing Emails</strong>: Default email only applies to outgoing emails without SMTP setting.</em></th></tr> + <tr><td width="180">Default Outgoing Email:</td> + <td> + <select name="default_smtp_id"> + <option value=0 selected="selected">None: Use PHP mail function</option> <?php - $sql='SELECT email_id,email,name,smtp_host FROM '.EMAIL_TABLE.' WHERE smtp_active=1'; - - if(($res=db_query($sql)) && db_num_rows($res)) { - while (list($id,$email,$name,$host) = db_fetch_row($res)){ - $email=$name?"$name <$email>":$email; - ?> - <option value="<?php echo $id; ?>"<?php echo ($config['default_smtp_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + $sql='SELECT email_id,email,name,smtp_host FROM '.EMAIL_TABLE.' WHERE smtp_active=1'; + + if(($res=db_query($sql)) && db_num_rows($res)) { + while (list($id,$email,$name,$host) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($config['default_smtp_id']==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> <?php - } - } ?> - </select> <font class="error"> <?php echo $errors['default_smtp_id']; ?></font> - </td> - </tr> - </tbody> -</table> -<p style="padding-left:250px;"> - <input class="button" type="submit" name="submit" value="Save Changes"> - <input class="button" type="reset" name="reset" value="Reset Changes"> -</p> -</form> + } + } ?> + </select> <font class="error"> <?php echo $errors['default_smtp_id']; ?></font> + </td> + </tr> + </tbody> +</table> +<p style="padding-left:250px;"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php index 6fe8433f5ee580c5a201a42518652106af63d313..0ab2ec09449a07f8f7efa78f8602683534b7666a 100644 --- a/include/staff/settings-kb.inc.php +++ b/include/staff/settings-kb.inc.php @@ -1,3 +1,7 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); +?> +<h2>Knowledge Base Settings and Options</h2> <form action="settings.php?t=kb" method="post" id="save"> <?php csrf_token(); ?> <input type="hidden" name="t" value="kb" > @@ -5,25 +9,25 @@ <thead> <tr> <th colspan="2"> - <h4>Knowledgebase Settings</h4> - <em>Disabling knowledgebase disables user's knowledgebase interface.</em> + <h4>Knowledge Base Settings</h4> + <em>Disabling knowledge base disables clients'interface.</em> </th> </tr> </thead> <tbody> <tr> - <td width="180">Knowledgebase Status:</td> + <td width="180">Knowledge base status:</td> <td> <input type="checkbox" name="enable_kb" value="1" <?php echo $config['enable_kb']?'checked="checked"':''; ?>> - Enable Knowledgebase <em>(Client Interface)</em> + Enable Knowledge base <em>(Client interface)</em> <font class="error"> <?php echo $errors['enable_kb']; ?></font> </td> </tr> <tr> - <td width="180">Premade Responses:</td> + <td width="180">Canned Responses:</td> <td> <input type="checkbox" name="enable_premade" value="1" <?php echo $config['enable_premade']?'checked="checked"':''; ?> > - Enable premade/canned responses <em>(Available on ticket reply)</em> + Enable canned responses <em>(Available on ticket reply)</em> <font class="error"> <?php echo $errors['enable_premade']; ?></font> </td> </tr> diff --git a/include/staff/settings-general.inc.php b/include/staff/settings-system.inc.php similarity index 66% rename from include/staff/settings-general.inc.php rename to include/staff/settings-system.inc.php index 20bbd94866960d2e06ae4d3451825d5752c853ac..3fba7f15977390d6882f00894523b90d92d4aced 100644 --- a/include/staff/settings-general.inc.php +++ b/include/staff/settings-system.inc.php @@ -1,12 +1,18 @@ -<form action="settings.php?t=general" method="post" id="save"> +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); + +$gmtime = Misc::gmtime(); +?> +<h2>System Settings and Preferences - <span>osTicket (v<?php echo $cfg->getVersion(); ?>)</span></h2> +<form action="settings.php?t=system" method="post" id="save"> <?php csrf_token(); ?> -<input type="hidden" name="t" value="general" > +<input type="hidden" name="t" value="system" > <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> <th colspan="2"> - <h4>General Settings</h4> - <em>Offline mode will disable client interface and only allow admins to login to Staff Control Panel</em> + <h4>System Settings & Preferences</h4> + <em><b>General Settings</b>: Offline mode will disable client interface and only allow admins to login to Staff Control Panel</em> </th> </tr> </thead> @@ -39,7 +45,7 @@ <?php $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' WHERE ispublic=1'; if(($res=db_query($sql)) && db_num_rows($res)){ - while (list($id,$name) = db_fetch_row($res)){ + while (list($id, $name) = db_fetch_row($res)){ $selected = ($config['default_dept_id']==$id)?'selected="selected"':''; ?> <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?> Dept</option> <?php @@ -56,7 +62,7 @@ <?php $sql='SELECT tpl_id,name FROM '.EMAIL_TEMPLATE_TABLE.' WHERE isactive=1 AND cfg_id='.db_input($cfg->getId()).' ORDER BY name'; if(($res=db_query($sql)) && db_num_rows($res)){ - while (list($id,$name) = db_fetch_row($res)){ + while (list($id, $name) = db_fetch_row($res)){ $selected = ($config['default_template_id']==$id)?'selected="selected"':''; ?> <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?></option> <?php @@ -113,26 +119,32 @@ <?php for ($i = 1; $i <= 12; $i++) { echo sprintf('<option value="%d" %s>%s%s</option>', - $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''),$i>1?"Every $i ":'',$i>1?' Months':'Monthly'); + $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''), $i>1?"Every $i ":'', $i>1?' Months':'Monthly'); } ?> </select> <font class="error"> <?php echo $errors['passwd_reset_period']; ?></font> </td> </tr> + <tr><td>Bind Staff Session to IP:</td> + <td> + <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> + <em>(binds staff session to originating IP address upon login)</em> + </td> + </tr> <tr><td>Staff Excessive Logins:</td> <td> <select name="staff_max_logins"> <?php for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_max_logins']==$i)?'selected="selected"':''),$i); + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i); } ?> </select> failed login attempt(s) allowed before a <select name="staff_login_timeout"> <?php for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['staff_login_timeout']==$i)?'selected="selected"':''),$i); + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i); } ?> </select> minute lock-out is enforced. @@ -144,18 +156,12 @@ Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). </td> </tr> - <tr><td>Bind Staff Session to IP:</td> - <td> - <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> - <em>(binds staff session to originating IP address upon login)</em> - </td> - </tr> <tr><td>Client Excessive Logins:</td> <td> <select name="client_max_logins"> <?php for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_max_logins']==$i)?'selected="selected"':''),$i); + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i); } ?> @@ -163,7 +169,7 @@ <select name="client_login_timeout"> <?php for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>',$i,(($config['client_login_timeout']==$i)?'selected="selected"':''),$i); + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i); } ?> </select> minute lock-out is enforced. @@ -176,16 +182,56 @@ Maximum idle time in minutes before a client must log in again (enter 0 to disable). </td> </tr> - <tr><td>Clickable URLs:</td> + <tr> + <th colspan="2"> + <em><b>Date and Time Options</b>: Please refer to <a href="http://php.net/date" target="_blank">PHP Manual</a> for supported parameters.</em> + </th> + </tr> + <tr><td width="220" class="required">Time Format:</td> + <td> + <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>"> + <font class="error">* <?php echo $errors['time_format']; ?></font> + <em><?php echo Format::date($config['time_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em></td> + </tr> + <tr><td width="220" class="required">Date Format:</td> + <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>"> + <font class="error">* <?php echo $errors['date_format']; ?></font> + <em><?php echo Format::date($config['date_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Date & Time Format:</td> + <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>"> + <font class="error">* <?php echo $errors['datetime_format']; ?></font> + <em><?php echo Format::date($config['datetime_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Day, Date & Time Format:</td> + <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>"> + <font class="error">* <?php echo $errors['daydatetime_format']; ?></font> + <em><?php echo Format::date($config['daydatetime_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em> + </td> + </tr> + <tr><td width="220" class="required">Default Time Zone:</td> <td> - <input type="checkbox" name="clickable_urls" <?php echo $config['clickable_urls']?'checked="checked"':''; ?>> - <em>(converts URLs in messages to clickable links)</em> + <select name="default_timezone_id"> + <option value="">— Select Default Time Zone —</option> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id, $offset, $tz)=db_fetch_row($res)){ + $sel=($config['default_timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>', $id, $sel, $offset, $tz); + } + } + ?> + </select> + <font class="error">* <?php echo $errors['default_timezone_id']; ?></font> </td> </tr> - <tr><td>Enable Auto Cron:</td> + <tr> + <td width="220">Daylight Saving:</td> <td> - <input type="checkbox" name="enable_auto_cron" <?php echo $config['enable_auto_cron']?'checked="checked"':''; ?>> - <em>(executes cron jobs based on staff activity - not recommended)</em> + <input type="checkbox" name="enable_daylight_saving" <?php echo $config['enable_daylight_saving'] ? 'checked="checked"': ''; ?>>Observe daylight savings </td> </tr> </tbody> @@ -195,4 +241,3 @@ <input class="button" type="reset" name="reset" value="Reset Changes"> </p> </form> - diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 280abca08ea44d698e0a41b56b1d8d0a62cd1693..992e9900425c19d708f7cb35d75a59f7b18c36bf 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -1,3 +1,9 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); +if(!($maxfileuploads=ini_get('max_file_uploads'))) + $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; +?> +<h2>Ticket Settings and Options</h2> <form action="settings.php?t=tickets" method="post" id="save"> <?php csrf_token(); ?> <input type="hidden" name="t" value="tickets" > @@ -5,7 +11,7 @@ <thead> <tr> <th colspan="2"> - <h4>Ticket Settings and Options</h4> + <h4>Ticket Settings</h4> <em>Global ticket settings and options.</em> </th> </tr> @@ -55,53 +61,59 @@ </td> </tr> <tr> - <td width="180">Web Tickets Priority</td> + <td>Maximum <b>Open</b> Tickets:</td> <td> - <input type="checkbox" name="allow_priority_change" value="1" <?php echo $config['allow_priority_change'] ?'checked="checked"':''; ?>> - <em>(Allow user to overwrite/set priority)</em> + <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>"> + per email/user. <em>(Helps with spam and email flood control - enter 0 for unlimited)</em> </td> </tr> <tr> - <td width="180">Emailed Tickets Priority</td> + <td>Ticket Auto-lock Time:</td> <td> - <input type="checkbox" name="use_email_priority" value="1" <?php echo $config['use_email_priority'] ?'checked="checked"':''; ?> > - <em>(Use email priority when available)</em> + <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>"> + <font class="error"><?php echo $errors['autolock_minutes']; ?></font> + <em>(Minutes to lock a ticket on activity - enter 0 to disable locking)</em> + </td> + </tr> + <tr> + <td width="180">Web Tickets Priority:</td> + <td> + <input type="checkbox" name="allow_priority_change" value="1" <?php echo $config['allow_priority_change'] ?'checked="checked"':''; ?>> + <em>(Allow user to overwrite/set priority)</em> + </td> + </tr> + <tr> + <td width="180">Emailed Tickets Priority:</td> + <td> + <input type="checkbox" name="use_email_priority" value="1" <?php echo $config['use_email_priority'] ?'checked="checked"':''; ?> > + <em>(Use email priority when available)</em> </td> </tr> <tr> - <td width="180">Show Related Tickets</td> + <td width="180">Show Related Tickets:</td> <td> <input type="checkbox" name="show_related_tickets" value="1" <?php echo $config['show_related_tickets'] ?'checked="checked"':''; ?> > <em>(Show all related tickets on user login - otherwise access is restricted to one ticket view per login)</em> </td> </tr> <tr> - <td width="180">Show Notes Inline</td> + <td width="180">Show Notes Inline:</td> <td> <input type="checkbox" name="show_notes_inline" value="1" <?php echo $config['show_notes_inline'] ?'checked="checked"':''; ?> > <em>(Show internal notes inline)</em> </td> - </tr> - <tr> - <td>Human Verification:</td> - <td> - <input type="checkbox" name="enable_captcha" <?php echo $config['enable_captcha']?'checked="checked"':''; ?>> - Enable CAPTCHA on new web tickets.<em>(requires GDLib)</em> <font class="error"> <?php echo $errors['enable_captcha']; ?></font><br/> - </td> </tr> - <tr> - <td>Maximum <b>Open</b> Tickets:</td> + <tr><td>Clickable URLs:</td> <td> - <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>"> - per email/user. <em>(Helps with spam and email flood control - enter 0 for unlimited)</em> + <input type="checkbox" name="clickable_urls" <?php echo $config['clickable_urls']?'checked="checked"':''; ?>> + <em>(converts URLs in ticket thread to clickable links)</em> </td> </tr> <tr> - <td>Ticket Auto-lock Time:</td> + <td>Human Verification:</td> <td> - <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>"> - <font class="error"><?php echo $errors['autolock_minutes']; ?></font> - <em>(Minutes to lock a ticket on activity - enter 0 to disable locking)</em> + <input type="checkbox" name="enable_captcha" <?php echo $config['enable_captcha']?'checked="checked"':''; ?>> + Enable CAPTCHA on new web tickets.<em>(requires GDLib)</em> <font class="error"> <?php echo $errors['enable_captcha']; ?></font><br/> </td> </tr> <tr> @@ -139,6 +151,94 @@ Hide staff's name on responses. </td> </tr> + <tr> + <th colspan="2"> + <em><b>Attachments</b>: Size setting mainly apply to web tickets.</em> + </th> + </tr> + <tr> + <td width="180">Allow Attachments:</td> + <td> + <input type="checkbox" name="allow_attachments" <?php echo $config['allow_attachments']?'checked="checked"':''; ?>><b>Allow Attachments</b> + <em>(Global Setting)</em> + <font class="error"> <?php echo $errors['allow_attachments']; ?></font> + </td> + </tr> + <tr> + <td width="180">Emailed Attachments:</td> + <td> + <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed files + <font class="error"> <?php echo $errors['allow_email_attachments']; ?></font> + </td> + </tr> + <tr> + <td width="180">Online Attachments:</td> + <td> + <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> > + Allow web upload + <input type="checkbox" name="allow_online_attachments_onlogin" <?php echo $config['allow_online_attachments_onlogin'] ?'checked="checked"':''; ?> > + Limit to authenticated users only. <em>(User must be logged in to upload files)</em> + <font class="error"> <?php echo $errors['allow_online_attachments']; ?></font> + </td> + </tr> + <tr> + <td>Max. User File Uploads:</td> + <td> + <select name="max_user_file_uploads"> + <?php + for($i = 1; $i <=$maxfileuploads; $i++) { + ?> + <option <?php echo $config['max_user_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> + <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> + <?php + } ?> + </select> + <em>(Number of files the user is allowed to upload simultaneously)</em> + <font class="error"> <?php echo $errors['max_user_file_uploads']; ?></font> + </td> + </tr> + <tr> + <td>Max. Staff File Uploads:</td> + <td> + <select name="max_staff_file_uploads"> + <?php + for($i = 1; $i <=$maxfileuploads; $i++) { + ?> + <option <?php echo $config['max_staff_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> + <?php echo $i; ?> <?php echo ($i>1)?'files':'file'; ?></option> + <?php + } ?> + </select> + <em>(Number of files the staff is allowed to upload simultaneously)</em> + <font class="error"> <?php echo $errors['max_staff_file_uploads']; ?></font> + </td> + </tr> + <tr> + <td width="180">Maximum File Size:</td> + <td> + <input type="text" name="max_file_size" value="<?php echo $config['max_file_size']; ?>"> in bytes. + <em>(System Max. <?php echo Format::file_size(ini_get('upload_max_filesize')); ?>)</em> + <font class="error"> <?php echo $errors['max_file_size']; ?></font> + </td> + </tr> + <tr> + <td width="180">Ticket Response Files:</td> + <td> + <input type="checkbox" name="email_attachments" <?php echo $config['email_attachments']?'checked="checked"':''; ?> >Email attachments to the user + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Accepted File Types</strong>: Limit the type of files users are allowed to submit. + <font class="error"> <?php echo $errors['allowed_filetypes']; ?></font></em> + </th> + </tr> + <tr> + <td colspan="2"> + <em>Enter allowed file extensions separated by a comma. e.g .doc, .pdf. To accept all files enter wildcard <b><i>.*</i></b> i.e dotStar (NOT Recommended).</em><br> + <textarea name="allowed_filetypes" cols="21" rows="4" style="width: 65%;" wrap="hard" ><?php echo $config['allowed_filetypes']; ?></textarea> + </td> + </tr> </tbody> </table> <p style="padding-left:250px;"> diff --git a/include/staff/slaplans.inc.php b/include/staff/slaplans.inc.php index c30d4459a24e824aa3724e844bdf0deaca14d532..a731b7492faa874a43e6aac9c6243faafdeed410 100644 --- a/include/staff/slaplans.inc.php +++ b/include/staff/slaplans.inc.php @@ -45,9 +45,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="slas.php?a=add" class="Icon newsla">Add New SLA Plan</a></b></div> <div class="clear"></div> -<form action="slas.php" method="POST" name="slas" onSubmit="return checkbox_checker(this,1,0);"> +<form action="slas.php" method="POST" name="slas"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -67,15 +68,14 @@ else if($res && db_num_rows($res)): while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> + </td> <td> <a href="slas.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a></td> <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> <td style="text-align:right;padding-right:35px;"><?php echo $row['grace_period']; ?> </td> @@ -90,9 +90,9 @@ else <td colspan="6"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['slas'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['slas'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['slas'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No SLA plans found'; } ?> @@ -104,16 +104,38 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected plans?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected plans?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected plans?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable" > + <input class="button" type="submit" name="delete" value="Delete" > </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected SLA plans? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected SLA plans? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected SLA plans?</strong></font> + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/staffmembers.inc.php b/include/staff/staffmembers.inc.php index d923815a0c876b8e5a4ff76d9b4ed96a12e90286..08877d02a7ab51b35f974b83183e04fd32566406 100644 --- a/include/staff/staffmembers.inc.php +++ b/include/staff/staffmembers.inc.php @@ -115,9 +115,10 @@ if($res && ($num=db_num_rows($res))) else $showing='No staff found!'; ?> -<form action="staff.php" method="POST" name="staff" onSubmit="return checkbox_checker(this,1,0);"> +<form action="staff.php" method="POST" name="staff" > <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -138,15 +139,12 @@ else $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['staff_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['staff_id'],$ids)) $sel=true; - } ?> - <tr id="<?php echo $row['dept_id']; ?>"> + <tr id="<?php echo $row['staff_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['staff_id']; ?>" <?php echo $sel?'checked="checked"':''; ?> - onClick="highLight(this.value,this.checked);"> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['staff_id']; ?>" <?php echo $sel?'checked="checked"':''; ?> > <td><a href="staff.php?id=<?php echo $row['staff_id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a> </td> <td><?php echo $row['username']; ?></td> <td><?php echo $row['isactive']?'Active':'<b>Locked</b>'; ?> <?php echo $row['onvacation']?'<small>(<i>vacation</i>)</small>':''; ?></td> @@ -163,9 +161,9 @@ else <td colspan="8"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['staff'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['staff'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['staff'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No staff members found!'; } ?> @@ -177,18 +175,43 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected users?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > - <input class="button" type="submit" name="disable" value="Lock" - onClick=' return confirm("Are you sure you want to LOCK selected users?");'> + <input class="button" type="submit" name="disable" value="Lock" > - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected users?");'> + <input class="button" type="submit" name="delete" value="Delete"> </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> (unlock) selected staff? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> (lock) selected staff? + <br><br>Locked staff won't be able to login to Staff Control Panel. + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected staff?</strong></font> + <br><br>Deleted staff CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php index ead5cf544932569261cecf166bfa0f152d309a76..de80a1dc35c309f9fa2f083b86ad331fcdcee362 100644 --- a/include/staff/syslogs.inc.php +++ b/include/staff/syslogs.inc.php @@ -104,9 +104,10 @@ else </div> </form> </div> -<form action="logs.php" method="POST" name="logs" onSubmit="return checkbox_checker(this,1,0);"> +<form action="logs.php" method="POST" name="logs"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -125,15 +126,13 @@ else if($res && db_num_rows($res)): while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['log_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['log_id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['log_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['log_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['log_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> </td> <td> <a class="tip" href="log/<?php echo $row['log_id']; ?>"><?php echo Format::htmlchars($row['title']); ?></a></td> <td><?php echo $row['log_type']; ?></td> <td> <?php echo Format::db_daydatetime($row['created']); ?></td> @@ -148,9 +147,9 @@ else <td colspan="6"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['logs'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['logs'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['logs'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No logs found'; } ?> @@ -162,11 +161,31 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="delete" value="Delete Selected Entries" - onClick=' return confirm("Are you sure you want to DELETE selected log entries?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="delete" value="Delete Selected Entries"> </p> <?php endif; ?> </form> + +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected logs?</strong></font> + <br><br>Deleted logs CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/teams.inc.php b/include/staff/teams.inc.php index d9dcee5bbc0031e2707394932294f9e5b88e612b..f63cdb40e6c864657b630b88b4ce059c17ab817e 100644 --- a/include/staff/teams.inc.php +++ b/include/staff/teams.inc.php @@ -44,9 +44,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="teams.php?a=add" class="Icon newteam">Add New Team</a></b></div> <div class="clear"></div> -<form action="teams.php" method="POST" name="teams" onSubmit="return checkbox_checker(this,1,0);"> +<form action="teams.php" method="POST" name="teams"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > + <input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -67,15 +68,13 @@ else if($res && db_num_rows($res)): while ($row = db_fetch_array($res)) { $sel=false; - if($ids && in_array($row['team_id'],$ids)){ - $class="$class highlight"; + if($ids && in_array($row['team_id'],$ids)) $sel=true; - } ?> <tr id="<?php echo $row['team_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['team_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['team_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?>> </td> <td><a href="teams.php?id=<?php echo $row['team_id']; ?>"><?php echo $row['name']; ?></a> </td> <td> <?php echo $row['isenabled']?'Active':'<b>Disabled</b>'; ?></td> <td style="text-align:right;padding-right:25px"> @@ -97,9 +96,9 @@ else <td colspan="7"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['teams'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['teams'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['teams'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No teams found!'; } ?> @@ -110,17 +109,38 @@ else <?php if($res && $num): //Show options.. ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected teams?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected teams?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected teams?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable" > + <input class="button" type="submit" name="delete" value="Delete" > </p> <?php endif; ?> - </form> - +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected teams? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected teams? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected teams?</strong></font> + <br><br>Deleted team CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php index dfdfb91c858d820b965867874282d76eeddab30c..a0f5de468a76bf7cde53e73bdc0767606ea6207b 100644 --- a/include/staff/templates.inc.php +++ b/include/staff/templates.inc.php @@ -48,9 +48,10 @@ else <div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> <b><a href="templates.php?a=add" class="Icon newEmailTemplate">Add New Template</a></b></div> <div class="clear"></div> -<form action="templates.php" method="POST" name="tpls" onSubmit="return checkbox_checker(this,1,0);"> +<form action="templates.php" method="POST" name="tpls"> <?php csrf_token(); ?> <input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> <caption><?php echo $showing; ?></caption> <thead> @@ -72,16 +73,16 @@ else 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"; + if($ids && in_array($row['tpl_id'],$ids)) $sel=true; - } + $default=($defaultTplId==$row['tpl_id'])?'<small class="fadded">(System Default)</small>':''; ?> <tr id="<?php echo $row['tpl_id']; ?>"> <td width=7px> - <input type="checkbox" name="ids[]" value="<?php echo $row['tpl_id']; ?>" - <?php echo $sel?'checked="checked"':''; ?> onClick="highLight(this.value,this.checked);"> </td> + <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['tpl_id']; ?>" + <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> > + </td> <td> <a href="templates.php?id=<?php echo $row['tpl_id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a> <?php echo $default; ?></td> <td> <?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></td> @@ -97,9 +98,9 @@ else <td colspan="6"> <?php if($res && $num){ ?> Select: - <a href="#" onclick="return select_all(document.forms['tpls'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['tpls'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['tpls'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo 'No templates found'; } ?> @@ -111,16 +112,39 @@ else if($res && $num): //Show options.. echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; ?> -<p class="centered"> - <input class="button" type="submit" name="enable" value="Enable" - onClick=' return confirm("Are you sure you want to ENABLE selected templates?");'> - <input class="button" type="submit" name="disable" value="Disable" - onClick=' return confirm("Are you sure you want to DISABLE selected templates?");'> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected templates?");'> +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="Enable" > + <input class="button" type="submit" name="disable" value="Disable" > + <input class="button" type="submit" name="delete" value="Delete" > </p> <?php endif; ?> </form> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + Are you sure want to <b>enable</b> selected templates? + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + Are you sure want to <b>disable</b> selected templates? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected templates?</strong></font> + <br><br>Deleted templates CANNOT be recovered. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php index 39093db710d6ca41cf681232ed4fa082b1278532..5f5a0a4d2e2e86816e64039bfb6ede821f5379b5 100644 --- a/include/staff/ticket-edit.inc.php +++ b/include/staff/ticket-edit.inc.php @@ -24,7 +24,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); Full Name: </td> <td> - <input type="text" size="45" name="name" value="<?php echo $info['name']; ?>"> + <input type="text" size="50" name="name" value="<?php echo $info['name']; ?>"> <span class="error">* <?php echo $errors['name']; ?></span> </td> </tr> @@ -33,7 +33,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); Email Address: </td> <td> - <input type="text" size="45" name="email" value="<?php echo $info['email']; ?>"> + <input type="text" size="50" name="email" value="<?php echo $info['email']; ?>"> <span class="error">* <?php echo $errors['email']; ?></span> </td> </tr> @@ -42,9 +42,9 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); Phone Number: </td> <td> - <input type="text" size="18" name="phone" value="<?php echo $info['phone']; ?>"> + <input type="text" size="20" name="phone" value="<?php echo $info['phone']; ?>"> <span class="error"> <?php echo $errors['phone']; ?></span> - Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> + Ext <input type="text" size="6" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> <span class="error"> <?php echo $errors['phone_ext']; ?></span> </td> </tr> @@ -109,11 +109,20 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); </tr> <tr> <td width="160" class="required"> - SLA: + Subject: + </td> + <td> + <input type="text" name="subject" size="60" value="<?php echo $info['subject']; ?>"> + <font class="error">* <?php $errors['subject']; ?></font> + </td> + </tr> + <tr> + <td width="160"> + SLA Plan: </td> <td> <select name="slaId"> - <option value="" selected >— Select SLA —</option> + <option value="0" selected="selected" >— None —</option> <?php if($slas=SLA::getSLAs()) { foreach($slas as $id =>$name) { @@ -123,16 +132,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); } ?> </select> - <font class="error">* <?php echo $errors['slaId']; ?></font> - </td> - </tr> - <tr> - <td width="160" class="required"> - Subject: - </td> - <td> - <input type="text" name="subject" size="60" value="<?php echo $info['subject']; ?>"> - <font class="error">* <?php $errors['subject']; ?></font> + <font class="error"> <?php echo $errors['slaId']; ?></font> </td> </tr> <tr> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 1ef3a7ba52ee0925ee4166984629d6661ce861a6..f36c1e0bcd97dcf1fd067fd71e585675283788f5 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -24,7 +24,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </td> <td> - <input type="text" size="45" name="email" id="email" class="typeahead" value="<?php echo $info['email']; ?>" + <input type="text" size="50" name="email" id="email" class="typeahead" value="<?php echo $info['email']; ?>" autocomplete="off" autocorrect="off" autocapitalize="off"> <span class="error">* <?php echo $errors['email']; ?></span> <?php @@ -40,7 +40,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Full Name: </td> <td> - <input type="text" size="45" name="name" id="name" value="<?php echo $info['name']; ?>"> + <input type="text" size="50" name="name" id="name" value="<?php echo $info['name']; ?>"> <span class="error">* <?php echo $errors['name']; ?></span> </td> </tr> @@ -49,15 +49,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Phone Number: </td> <td> - <input type="text" size="18" name="phone" id="phone" value="<?php echo $info['phone']; ?>"> + <input type="text" size="20" name="phone" id="phone" value="<?php echo $info['phone']; ?>"> <span class="error"> <?php echo $errors['phone']; ?></span> - Ext <input type="text" size="5" name="phone_ext" id="phone_ext" value="<?php echo $info['phone_ext']; ?>"> + Ext <input type="text" size="6" name="phone_ext" id="phone_ext" value="<?php echo $info['phone_ext']; ?>"> <span class="error"> <?php echo $errors['phone_ext']; ?></span> </td> </tr> <tr> <th colspan="2"> - <em><strong>Ticket Information</strong>:</em> + <em><strong>Ticket Information & Options</strong>:</em> </th> </tr> <tr> @@ -114,92 +114,45 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </td> </tr> <tr> - <td width="160" class="required"> - Subject: + <td width="160"> + Priority: </td> <td> - <input type="text" name="subject" size="55" value="<?php echo $info['subject']; ?>"> - <font class="error">* <?php echo $errors['subject']; ?></font> + <select name="priorityId"> + <option value="0" selected >— System Default —</option> + <?php + if($priorities=Priority::getPriorities()) { + foreach($priorities as $id =>$name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['priorityId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> + <font class="error"> <?php echo $errors['priorityId']; ?></font> </td> - </tr> - <tr> - <th colspan="2"> - <em><strong>Issue summary</strong>: Detailed summary of the reason(s) of opening the ticket. <font class="error">* <?php echo $errors['issue']; ?></font></em> - </th> - </tr> - <tr> - <td colspan=2> - <textarea name="issue" cols="21" rows="8" style="width:80%;"><?php echo $info['issue']; ?></textarea> - <br><em>The user will be able to see the summary and any associated responses.</em> + </tr> + <tr> + <td width="160"> + SLA Plan: </td> - </tr> - <tr> - <th colspan="2"> - <em><strong>Response</strong>: Optional response to the above issue.</em> - </th> - </tr> - <tr> - <td colspan=2> - <?php - if(($cannedResponses=Canned::getCannedResponses())) { - ?> - <div> - Canned Response: - <select id="cannedResp" name="cannedResp"> - <option value="0" selected="selected">— Select a canned response —</option> - <?php - foreach($cannedResponses as $id =>$title) { - echo sprintf('<option value="%d">%s</option>',$id,$title); - } - ?> - </select> - - <label><input type='checkbox' value='1' name="append" id="append" checked="checked">Append</label> - </div> - <?php - } ?> - <textarea name="response" id="response" cols="21" rows="8" style="width:80%;"><?php echo $info['response']; ?></textarea> - <?php - if($cfg->allowAttachments()) { ?> - <br><em><b>Attachments:</b> Response required when files are attached.</em> - <div class="canned_attachments"> + <td> + <select name="slaId"> + <option value="0" selected="selected" >— System Default —</option> <?php - if($info['cannedattachments']) { - foreach($info['cannedattachments'] as $k=>$id) { - if(!($file=AttachmentFile::lookup($id))) continue; - $hash=$file->getHash().md5($file->getId().session_id().$file->getHash()); - echo sprintf('<label><input type="checkbox" name="cannedattachments[]" id="f%d" value="%d" checked="checked"> - <a href="file.php?h=%s">%s</a> </label> ', - $file->getId(), $file->getId() , $hash, $file->getName()); + if($slas=SLA::getSLAs()) { + foreach($slas as $id =>$name) { + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['slaId']==$id)?'selected="selected"':'',$name); } } ?> - </div> - <div class="uploads"></div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> - <?php - } ?> + </select> + <font class="error"> <?php echo $errors['slaId']; ?></font> </td> - </tr> + </tr> - <tr> - <th colspan="2"> - <em><strong>Internal Note</strong>: Optional internal note (recommended on assignment) <font class="error"> <?php echo $errors['note']; ?></font></em> - </th> - </tr> - <tr> - <td colspan=2> - <textarea name="note" cols="21" rows="6" style="width:80%;"><?php echo $info['note']; ?></textarea> - </td> - </tr> - <tr> - <th colspan="2"> - <em><strong>Ticket Options</strong>: Due date, when set, overwrites SLA grace period.</em> - </th> - </tr> - <tr> + <tr> <td width="160"> Due Date: </td> @@ -210,32 +163,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); $min=$hr=null; if($info['time']) list($hr, $min)=explode(':', $info['time']); - + echo Misc::timeDropdown($hr, $min, 'time'); ?> <font class="error"> <?php echo $errors['duedate']; ?> <?php echo $errors['time']; ?></font> <em>Time is based on your time zone (GMT <?php echo $thisstaff->getTZoffset(); ?>)</em> </td> </tr> - <tr> - <td width="160"> - Priority: - </td> - <td> - <select name="priorityId"> - <option value="0" selected >— System Default —</option> - <?php - if($priorities=Priority::getPriorities()) { - foreach($priorities as $id =>$name) { - echo sprintf('<option value="%d" %s>%s</option>', - $id, ($info['priorityId']==$id)?'selected="selected"':'',$name); - } - } - ?> - </select> - <font class="error"> <?php echo $errors['priorityId']; ?></font> - </td> - </tr> + <?php if($thisstaff->canAssignTickets()) { ?> <tr> @@ -253,7 +188,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); } echo '</OPTGROUP>'; } - + if(($teams=Team::getActiveTeams())) { echo '<OPTGROUP label="Teams ('.count($teams).')">'; foreach($teams as $id => $name) { @@ -269,37 +204,114 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <?php } ?> - - <?php - if($thisstaff->canCloseTickets()) { ?> <tr> - <td width="160"> - Ticket Status: - </td> - <td> - <input type="checkbox" name="ticket_state" value="closed" <?php echo $info['ticket_state']?'checked="checked"':''; ?>> - <b>Close On Response</b> <em>(Only applicable if response is entered)</em> + <th colspan="2"> + <em><strong>Issue</strong>: The user will be able to see the issue summary below and any associated responses.</em> + </th> + </tr> + <tr> + <td colspan=2> + <div> + <em><strong>Subject</strong>: Issue summary </em> <font class="error">* <?php echo $errors['subject']; ?></font><br> + <input type="text" name="subject" size="60" value="<?php echo $info['subject']; ?>"> + </div> + <div><em><strong>Issue</strong>: Details on the reason(s) for opening the ticket.</em> <font class="error">* <?php echo $errors['issue']; ?></font></div> + <textarea name="issue" cols="21" rows="8" style="width:80%;"><?php echo $info['issue']; ?></textarea> </td> </tr> - <?php - } ?> <tr> - <td>Signature:</td> - <td> - <?php - $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); + <th colspan="2"> + <em><strong>Response</strong>: Optional response to the above issue.</em> + </th> + </tr> + <tr> + <td colspan=2> + <?php + if(($cannedResponses=Canned::getCannedResponses())) { ?> - <label><input type="radio" name="signature" value="none" checked="checked"> None</label> - <?php - if($thisstaff->getSignature()) { ?> - <label><input type="radio" name="signature" value="mine" - <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> + <div> + Canned Response: + <select id="cannedResp" name="cannedResp"> + <option value="0" selected="selected">— Select a canned response —</option> + <?php + foreach($cannedResponses as $id =>$title) { + echo sprintf('<option value="%d">%s</option>',$id,$title); + } + ?> + </select> + + <label><input type='checkbox' value='1' name="append" id="append" checked="checked">Append</label> + </div> + <?php + } ?> + <textarea name="response" id="response" cols="21" rows="8" style="width:80%;"><?php echo $info['response']; ?></textarea> + <table border="0" cellspacing="0" cellpadding="2" width="100%"> <?php - } ?> - <label><input type="radio" name="signature" value="dept" - <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> - Dept. Signature (if set)</label> - <span style="padding-left:25px;"> + if($cfg->allowAttachments()) { ?> + <tr><td width="100" valign="top">Attachments:</td> + <td> + <div class="canned_attachments"> + <?php + if($info['cannedattachments']) { + foreach($info['cannedattachments'] as $k=>$id) { + if(!($file=AttachmentFile::lookup($id))) continue; + $hash=$file->getHash().md5($file->getId().session_id().$file->getHash()); + echo sprintf('<label><input type="checkbox" name="cannedattachments[]" + id="f%d" value="%d" checked="checked" + <a href="file.php?h=%s">%s</a> </label> ', + $file->getId(), $file->getId() , $hash, $file->getName()); + } + } + ?> + </div> + <div class="uploads"></div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + </td> + </tr> + <?php + } ?> + + <?php + if($thisstaff->canCloseTickets()) { ?> + <tr> + <td width="100">Ticket Status:</td> + <td> + <input type="checkbox" name="ticket_state" value="closed" <?php echo $info['ticket_state']?'checked="checked"':''; ?>> + <b>Close On Response</b> <em>(Only applicable if response is entered)</em> + </td> + </tr> + <?php + } ?> + <tr> + <td width="100">Signature:</td> + <td> + <?php + $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); + ?> + <label><input type="radio" name="signature" value="none" checked="checked"> None</label> + <?php + if($thisstaff->getSignature()) { ?> + <label><input type="radio" name="signature" value="mine" + <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> + <?php + } ?> + <label><input type="radio" name="signature" value="dept" + <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> Dept. Signature (if set)</label> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <th colspan="2"> + <em><strong>Internal Note</strong>: Optional internal note (recommended on assignment) <font class="error"> <?php echo $errors['note']; ?></font></em> + </th> + </tr> + <tr> + <td colspan=2> + <textarea name="note" cols="21" rows="6" style="width:80%;"><?php echo $info['note']; ?></textarea> </td> </tr> </tbody> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index a58206963acf4deb36fd48bf17d80c2ea63020aa..c2cb0bd1067684f7a012aa4f1a44cc73dc8ee904 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -1,650 +1,851 @@ -<?php -//Note that ticket obj is initiated in tickets.php. -if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path'); - -//Make sure the staff is allowed to access the page. -if(!@$thisstaff->isStaff() || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied'); - -//Re-use the post info on error...savekeyboards.org (Why keyboard? -> some people care about objects than users!!) -$info=($_POST && $errors)?Format::input($_POST):array(); - -//Auto-lock the ticket if locking is enabled.. If already locked by the user then it simply renews. -if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime())) - $warn.='Unable to obtain a lock on the ticket'; - -//Get the goodies. -$dept = $ticket->getDept(); //Dept -$staff = $ticket->getStaff(); //Assigned or closed by.. -$team = $ticket->getTeam(); //Assigned team. -$lock = $ticket->getLock(); //Ticket lock obj -$id = $ticket->getId(); //Ticket ID. - -//Useful warnings and errors the user might want to know! -if($ticket->isAssigned() && ( - ($staff && $staff->getId()!=$thisstaff->getId()) - || ($team && !$team->hasMember($thisstaff)) - )) - $warn.=' <span class="Icon assignedTicket">Ticket is assigned to '.implode('/', $ticket->getAssignees()).'</span>'; -if(!$errors['err'] && ($lock && $lock->getStaffId()!=$thisstaff->getId())) - $errors['err']='This ticket is currently locked by '.$lock->getStaffName(); -if(!$errors['err'] && ($emailBanned=EmailFilter::isBanned($ticket->getEmail()))) - $errors['err']='Email is in banlist! Must be removed before any reply/response'; - -$unbannable=($emailBanned) ? BanList::includes($ticket->getEmail()) : false; - -if($ticket->isOverdue()) - $warn.=' <span class="Icon overdueTicket">Marked overdue!</span>'; - -?> -<table width="910" cellpadding="2" cellspacing="0" border="0"> - <tr> - <td width="50%"> - <h2>Ticket #<?php echo $ticket->getExtId(); ?> - <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload" class="reload">Reload</a></h2> - </td> - <td width="50%" class="right_align"> - <a href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=print" title="Print Ticket" class="print" id="ticket-print">Print Ticket</a> - <?php - if($thisstaff->canEditTickets()) { ?> - <a href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit" title="Edit Ticket" class="edit">Edit Ticket</a> - <?php - } ?> - </td> - </tr> -</table> -<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> - <tr> - <td width="50"> - <table border="0" cellspacing="" cellpadding="4" width="100%"> - <tr> - <th width="100">Status:</th> - <td><?php echo ucfirst($ticket->getStatus()); ?></td> - </tr> - <tr> - <th>Priority:</th> - <td><?php echo $ticket->getPriority(); ?></td> - </tr> - <tr> - <th>Department:</th> - <td><?php echo Format::htmlchars($ticket->getDeptName()); ?></td> - </tr> - <tr> - <th>Create Date:</th> - <td><?php echo Format::db_datetime($ticket->getCreateDate()); ?></td> - </tr> - </table> - </td> - <td width="50%"> - <table border="0" cellspacing="" cellpadding="4" width="100%"> - <tr> - <th width="100">Name:</th> - <td><?php echo Format::htmlchars($ticket->getName()); ?></td> - </tr> - <tr> - <th>Email:</th> - <td> - <?php - echo $ticket->getEmail(); - if(($related=$ticket->getRelatedTicketsCount())) { - echo sprintf(' <a href="tickets.php?a=search&query=%s" title="Related Tickets">(<b>%d</b>)</a>', - urlencode($ticket->getEmail()),$related); - - } - ?> - </td> - </tr> - <tr> - <th>Phone:</th> - <td><?php echo $ticket->getPhoneNumber(); ?></td> - </tr> - <tr> - <th>Source:</th> - <td><?php echo Format::htmlchars($ticket->getSource()); ?></td> - </tr> - </table> - </td> - </tr> -</table> -<br> -<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> - <tr> - <td width="50%"> - <table cellspacing="0" cellpadding="4" width="100%" border="0"> - <?php - if($ticket->isOpen()) { ?> - <tr> - <th width="100">Assigned To:</th> - <td> - <?php - if($ticket->isAssigned()) - echo Format::htmlchars(implode('/', $ticket->getAssignees())); - else - echo '<span class="faded">— Unassigned —</span>'; - ?> - </td> - </tr> - <?php - } else { ?> - <tr> - <th width="100">Closed By:</th> - <td> - <?php - if(($staff = $ticket->getStaff())) - echo Format::htmlchars($staff->getName()); - else - echo '<span class="faded">— Unknown —</span>'; - ?> - </td> - </tr> - <?php - } ?> - <tr> - <th nowrap>Last Response:</th> - <td><?php echo Format::db_datetime($ticket->getLastRespDate()); ?></td> - </tr> - <?php - if($ticket->isOpen()){ ?> - <tr> - <th>Due Date:</th> - <td><?php echo Format::db_datetime($ticket->getDueDate()); ?></td> - </tr> - <?php - }else { ?> - <tr> - <th>Close Date:</th> - <td><?php echo Format::db_datetime($ticket->getCloseDate()); ?></td> - </tr> - <?php - } - ?> - </table> - </td> - <td width="50%"> - <table cellspacing="0" cellpadding="4" width="100%" border="0"> - <tr> - <th width="100">Subject:</th> - <td><?php echo Format::htmlchars(Format::truncate($ticket->getSubject(),200)); ?></td> - </tr> - <tr> - <th>Help Topic:</th> - <td><?php echo Format::htmlchars($ticket->getHelpTopic()); ?></td> - </tr> - <tr> - <th nowrap>Last Message:</th> - <td><?php echo Format::db_datetime($ticket->getLastMsgDate()); ?></td> - </tr> - </table> - </td> - </tr> -</table> -<div class="clear" style="padding-bottom:10px;"></div> -<?php -$tcount = $ticket->getThreadCount(); -if($cfg->showNotesInline()) - $tcount+= $ticket->getNumNotes(); -?> -<ul id="threads"> - <li><a class="active" id="toggle_ticket_thread" href="#">Ticket Thread (<?php echo $tcount; ?>)</a></li> - <?php - if(!$cfg->showNotesInline()) {?> - <li><a id="toggle_notes" href="#">Internal Notes (<?php echo $ticket->getNumNotes(); ?>)</a></li> - <?php - }?> -</ul> -<?php -if(!$cfg->showNotesInline()) { ?> -<div id="ticket_notes"> - <?php - /* Internal Notes */ - if($ticket->getNumNotes() && ($notes=$ticket->getNotes())) { - foreach($notes as $note) { - - ?> - <table class="note" cellspacing="0" cellpadding="1" width="940" border="0"> - <tr> - <th width="640"> - <?php - echo sprintf('%s <em>posted by <b>%s</b></em>', - Format::htmlchars($note['title']), - Format::htmlchars($note['poster'])); - ?> - </th> - <th class="date" width="300"><?php echo Format::db_datetime($note['created']); ?></th> - </tr> - <tr> - <td colspan="2"> - <?php echo Format::display($note['body']); ?> - </td> - </tr> - <?php - if($note['attachments'] && ($links=$ticket->getAttachmentsLinks($note['id'],'N'))) {?> - <tr> - <td class="info" colspan="2"><?php echo $links; ?></td> - </tr> - <?php - }?> - </table> - <?php - } - } else { - echo "<p>No internal notes found.</p>"; - }?> -</div> -<?php -} ?> -<div id="ticket_thread"> - <?php - $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note'); - /* -------- Messages & Responses & Notes (if inline)-------------*/ - if(($thread=$ticket->getThread($cfg->showNotesInline()))) { - foreach($thread as $entry) { - ?> - <table class="<?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> - <tr> - <th width="200"><?php echo Format::db_datetime($entry['created']);?></th> - <th width="440"><span><?php echo Format::htmlchars($entry['title']); ?></span></th> - <th width="300" class="tmeta"><?php echo Format::htmlchars($entry['poster']); ?></th> - </tr> - <tr><td colspan=3><?php echo Format::display($entry['body']); ?></td></tr> - <?php - if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) {?> - <tr> - <td class="info" colspan=3><?php echo $links; ?></td> - </tr> - <?php - }?> - </table> - <?php - if($entry['thread_type']=='M') - $msgId=$entry['id']; - } - } else { - echo '<p>Error fetching ticket thread - get technical help.</p>'; - }?> -</div> -<div class="clear" style="padding-bottom:10px;"></div> -<?php if($errors['err']) { ?> - <div id="msg_error"><?php echo $errors['err']; ?></div> -<?php }elseif($msg) { ?> - <div id="msg_notice"><?php echo $msg; ?></div> -<?php }elseif($warn) { ?> - <div id="msg_warning"><?php echo $warn; ?></div> -<?php } ?> - -<div id="response_options"> - <ul> - <li><a id="reply_tab" href="#reply">Post Reply</a></li> - <li><a id="note_tab" href="#note">Post Internal Note</a></li> - <?php - if($thisstaff->canTransferTickets()) { ?> - <li><a id="transfer_tab" href="#transfer">Dept. Transfer</a></li> - <?php - } - - if($thisstaff->canAssignTickets()) { ?> - <li><a id="assign_tab" href="#assign"><?php echo $ticket->isAssigned()?'Reassign Ticket':'Assign Ticket'; ?></a></li> - <?php - } ?> - </ul> - - <form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> - <?php csrf_token(); ?> - <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> - <input type="hidden" name="msgId" value="<?php echo $msgId; ?>"> - <input type="hidden" name="a" value="reply"> - <span class="error"></span> - <table border="0" cellspacing="0" cellpadding="3"> - <tr> - <td width="160"> </td> - <td class="error"><?php echo $errors['response']; ?></td> - </tr> - <?php - if(($cannedResponses=Canned::responsesByDeptId($ticket->getDeptId()))) {?> - <tr> - <td width="160"> - <label> </label> - </td> - <td width="765"> - <select id="cannedResp" name="cannedResp"> - <option value="0" selected="selected">Select a canned response</option> - <?php - foreach($cannedResponses as $id =>$title) { - echo sprintf('<option value="%d">%s</option>',$id,$title); - } - ?> - </select> - - <label><input type='checkbox' value='1' name="append" id="append" checked="checked"> Append</label> - </td> - </tr> - <?php - }?> - <tr> - <td width="160"> - <label><strong>Response:</strong></label> - </td> - <td width="765"> - <textarea name="response" id="response" cols="50" rows="9" wrap="soft"><?php echo $info['response']; ?></textarea> - </td> - </tr> - <?php - if($cfg->allowAttachments()) { ?> - <tr> - <td width="160"> - <label for="attachment">Attachments:</label> - </td> - <td width="765" id="reply_form_attachments" class="attachments"> - <div class="canned_attachments"> - </div> - <div class="uploads"> - </div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> - </td> - </tr> - <?php - }?> - <tr> - <td width="160"> - <label for="signature" class="left">Signature:</label> - </td> - <td width="765"> - <?php - $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); - ?> - <label><input type="radio" name="signature" value="none" checked="checked"> None</label> - <?php - if($thisstaff->getSignature()) {?> - <label><input type="radio" name="signature" value="mine" - <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> - <?php - } ?> - <?php - if($dept && $dept->canAppendSignature()) { ?> - <label><input type="radio" name="signature" value="dept" - <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> - Dept. Signature (<?php echo Format::htmlchars($dept->getName()); ?>)</label> - <?php - } ?> - </td> - </tr> - <?php - if($ticket->isClosed() || $thisstaff->canCloseTickets()) { ?> - <tr> - <td width="160"> - <label><strong>Ticket Status:</strong></label> - </td> - <td width="765"> - <?php - $statusChecked=isset($info['reply_ticket_status'])?'checked="checked"':''; - if($ticket->isClosed()) { ?> - <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Open" - <?php echo $statusChecked; ?>> Reopen on Reply</label> - <?php - } elseif($thisstaff->canCloseTickets()) { ?> - <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Closed" - <?php echo $statusChecked; ?>> Close on Reply</label> - <?php - } ?> - </td> - </tr> - <?php - } ?> - </div> - </table> - <p style="padding-left:165px;"> - <input class="btn_sm" type="submit" value="Post Reply"> - <input class="btn_sm" type="reset" value="Reset"> - </p> - </form> - <form id="note" action="tickets.php?id=<?php echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data"> - <?php csrf_token(); ?> - <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> - <input type="hidden" name="a" value="postnote"> - <table border="0" cellspacing="0" cellpadding="3"> - <tr> - <td width="160"> </td> - <td class="error"><?php echo $errors['note']; ?></td> - </tr> - <tr> - <td width="160"> - <label><strong>Title:</strong></label> - </td> - <td width="765"> - <input type="text" name="title" id="title" size="45" value="<?php echo $info['title']; ?>" > - <span class="error">* <?php echo $errors['title']; ?></span> - </td> - </tr> - <tr> - <td width="160"> - <label><strong>Note:</strong></label> - </td> - <td width="765"> - <div><span class="faded">Internal note details</span> - <span class="error">* <?php echo $errors['internal_note']; ?></span></div> - <textarea name="internal_note" id="internal_note" cols="50" rows="9" wrap="soft" - style="width:600px"><?php echo $info['internal_note']; ?></textarea> - </td> - </tr> - - <?php - if($cfg->allowAttachments()) { ?> - <tr> - <td width="160"> - <label for="attachment">Attachments:</label> - </td> - <td width="765" class="attachments"> - <div class="uploads"> - </div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> - </td> - </tr> - <?php - } - ?> - <tr> - <td width="160"> - <label>Ticket Status:</label> - </td> - <td width="765"> - <?php - $statusChecked=isset($info['note_ticket_state'])?'checked="checked"':''; - if($ticket->isClosed()){ ?> - <label><input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="open" - <?php echo $statusChecked; ?>> Reopen Ticket</label> - <?php - } elseif(0 && $thisstaff->canCloseTickets()) { ?> - <label><input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Closed" - <?php echo $statusChecked; ?>> Close Ticket</label> - <?php - } elseif($ticket->isAnswered()) { ?> - <label> - <input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Unanswered" - <?php echo $statusChecked; ?>> - Mark Unanswered - </label> - <?php - } else { ?> - <label> - <input type="checkbox" name="note_ticket_state" id="note_ticket_state" value="Answered" - <?php echo $statusChecked; ?>> - Mark Answered - </label> - <?php - } ?> - </td> - </tr> - </div> - </table> - - <p style="padding-left:165px;"> - <input class="btn_sm" type="submit" value="Post Note"> - <input class="btn_sm" type="reset" value="Reset"> - </p> - </form> - <?php - if($thisstaff->canTransferTickets()) { ?> - <form id="transfer" action="tickets.php?id=<?php echo $ticket->getId(); ?>#transfer" name="transfer" method="post" enctype="multipart/form-data"> - <?php csrf_token(); ?> - <input type="hidden" name="ticket_id" value="<?php echo $ticket->getId(); ?>"> - <input type="hidden" name="a" value="transfer"> - <table border="0" cellspacing="0" cellpadding="3"> - <tr> - <td width="160"> </td> - <td class="error"><?php echo $errors['transfer']; ?></td> - </tr> - <tr> - <td width="160"> - <label for="deptId"><strong>Department:</strong></label> - </td> - <td width="765"> - <select id="deptId" name="deptId"> - <option value="0" selected="selected">— Select Target Department —</option> - <?php - if($depts=Dept::getDepartments()) { - foreach($depts as $id =>$name) { - if($id==$ticket->getDeptId()) continue; - echo sprintf('<option value="%d" %s>%s</option>', - $id, ($info['deptId']==$id)?'selected="selected"':'',$name); - } - } - ?> - </select> <span class='error'>* <?php echo $errors['deptId']; ?></span> - </td> - </tr> - <tr> - <td width="160"> - <label><strong>Comments:</strong></label> - </td> - <td width="765"> - <span class="faded">Enter reasons for the transfer.</span> - <span class="error">* <?php echo $errors['transfer_message']; ?></span><br> - <textarea name="transfer_message" id="transfer_message" - cols="80" rows="7" wrap="soft"><?php echo $info['transfer_message']; ?></textarea> - </td> - </tr> - </table> - <p style="padding-left:165px;"> - <input class="btn_sm" type="submit" value="Transfer"> - <input class="btn_sm" type="reset" value="Reset"> - </p> - </form> - <?php - } ?> - <?php - if($thisstaff->canAssignTickets()) { ?> - <form id="assign" action="tickets.php?id=<?php echo $ticket->getId(); ?>#assign" name="assign" method="post" enctype="multipart/form-data"> - <?php csrf_token(); ?> - <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> - <input type="hidden" name="a" value="assign"> - <table border="0" cellspacing="0" cellpadding="3"> - <tr> - <td width="160"> </td> - <td> - <?php - if($ticket->isAssigned()) - echo sprintf('<em>Ticket is currently assigned to <b>%s</b></em>',$ticket->getAssignee()); - ?> - </td> - </tr> - <tr> - <td width="160"> - <label for="assignId"><strong>Assignee:</strong></label> - </td> - <td width="765"> - <select id="assignId" name="assignId"> - <option value="0" selected="selected">— Select Staff Member OR a Team —</option> - <?php - $sid=$tid=0; - if(($users=Staff::getAvailableStaffMembers())) { - echo '<OPTGROUP label="Staff Members ('.count($users).')">'; - $staffId=$ticket->isAssigned()?$ticket->getStaffId():0; - foreach($users as $id => $name) { - if($staffId && $staffId==$id) - $name.=' (Assigned)'; - - $k="s$id"; - echo sprintf('<option value="%s" %s>%s</option>', - $k,(($info['assignId']==$k)?'selected="selected"':''),$name); - } - echo '</OPTGROUP>'; - } - - if(($teams=Team::getActiveTeams())) { - echo '<OPTGROUP label="Teams ('.count($teams).')">'; - $teamId=(!$sid && $ticket->isAssigned())?$ticket->getTeamId():0; - foreach($teams as $id => $name) { - if($teamId && $teamId==$id) - $name.=' (Assigned)'; - - $k="t$id"; - echo sprintf('<option value="%s" %s>%s</option>', - $k,(($info['assignId']==$k)?'selected="selected"':''),$name); - } - echo '</OPTGROUP>'; - } - ?> - </select> <span class='error'>* <?php echo $errors['assignId']; ?></span> - </td> - </tr> - <tr> - <td width="160"> - <label><strong>Comments:</strong><span class='error'> </span></label> - </td> - <td width="765"> - <span class="faded">Enter reasons for the assignment or instructions.</span> - <span class="error">* <?php echo $errors['assign_message']; ?></span><br> - <textarea name="assign_message" id="assign_message" cols="80" rows="7" wrap="soft"><?php echo $info['assign_message']; ?></textarea> - </td> - </tr> - </table> - <p style="padding-left:165px;"> - <input class="btn_sm" type="submit" value="<?php echo $ticket->isAssigned()?'Reassign':'Assign'; ?>"> - <input class="btn_sm" type="reset" value="Reset"> - </p> - </form> - <?php - } ?> -</div> -<div style="display:none;" id="print-options"> - <h3>Ticket Print Options</h3> - <a class="close" href="">×</a> - <hr/> - <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post" id="print-form" name="print-form"> - <?php csrf_token(); ?> - <input type="hidden" name="a" value="print"> - <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> - <fieldset class="notes"> - <label for="notes">Print Notes:</label> - <input type="checkbox" id="notes" name="notes" value="1"> Print <b>Internal</b> Notes/Comments - </fieldset> - <fieldset> - <label for="psize">Paper Size:</label> - <select id="psize" name="psize"> - <option value="">— Select Print Paper Size —</option> - <?php - $options=array('Letter', 'Legal', 'A4', 'A3'); - $psize =$_SESSION['PAPER_SIZE']?$_SESSION['PAPER_SIZE']:$thisstaff->getDefaultPaperSize(); - foreach($options as $v) { - echo sprintf('<option value="%s" %s>%s</option>', - $v,($psize==$v)?'selected="selected"':'', $v); - } - ?> - </select> - </fieldset> - <hr style="margin-top:3em"/> - <p class="full-width"> - <span class="buttons" style="float:left"> - <input type="reset" value="Reset"> - <input type="button" value="Cancel" class="close"> - </span> - <span class="buttons" style="float:right"> - <input type="submit" value="Print"> - </span> - </p> - </form> -</div> -<script type="text/javascript" src="js/ticket.js"></script> +<?php +//Note that ticket obj is initiated in tickets.php. +if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path'); + +//Make sure the staff is allowed to access the page. +if(!@$thisstaff->isStaff() || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied'); + +//Re-use the post info on error...savekeyboards.org (Why keyboard? -> some people care about objects than users!!) +$info=($_POST && $errors)?Format::input($_POST):array(); + +//Auto-lock the ticket if locking is enabled.. If already locked by the user then it simply renews. +if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime())) + $warn.='Unable to obtain a lock on the ticket'; + +//Get the goodies. +$dept = $ticket->getDept(); //Dept +$staff = $ticket->getStaff(); //Assigned or closed by.. +$team = $ticket->getTeam(); //Assigned team. +$sla = $ticket->getSLA(); +$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 && $staff->getId()!=$thisstaff->getId()) + || ($team && !$team->hasMember($thisstaff)) + )) + $warn.=' <span class="Icon assignedTicket">Ticket is assigned to '.implode('/', $ticket->getAssignees()).'</span>'; +if(!$errors['err'] && ($lock && $lock->getStaffId()!=$thisstaff->getId())) + $errors['err']='This ticket is currently locked by '.$lock->getStaffName(); +if(!$errors['err'] && ($emailBanned=TicketFilter::isBanned($ticket->getEmail()))) + $errors['err']='Email is in banlist! Must be removed before any reply/response'; + +$unbannable=($emailBanned) ? BanList::includes($ticket->getEmail()) : false; + +if($ticket->isOverdue()) + $warn.=' <span class="Icon overdueTicket">Marked overdue!</span>'; + +?> +<table width="940" cellpadding="2" cellspacing="0" border="0"> + <tr> + <td width="50%" class="has_bottom_border"> + <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><i class="icon-refresh"></i> Ticket #<?php echo $ticket->getExtId(); ?></a></h2> + </td> + <td width="50%" class="right_align has_bottom_border"> + <?php + if($thisstaff->canBanEmails() || ($dept && $dept->isManager($thisstaff))) { ?> + <span class="action-button" data-dropdown="#action-dropdown-more"> + <span ><i class="icon-cog"></i> More</span> + <i class="icon-caret-down"></i> + </span> + <?php + } ?> + <?php if($thisstaff->canDeleteTickets()) { ?> + <a id="ticket-delete" class="action-button" href="#delete"><i class="icon-trash"></i> Delete</a> + <?php } ?> + <?php + if($thisstaff->canCloseTickets()) { + if($ticket->isOpen()) {?> + <a id="ticket-close" class="action-button" href="#close"><i class="icon-remove-circle"></i> Close</a> + <?php + } else { ?> + <a id="ticket-reopen" class="action-button" href="#reopen"><i class="icon-undo"></i> Reopen</a> + <?php + } ?> + <?php + } ?> + <?php + if($thisstaff->canEditTickets()) { ?> + <a class="action-button" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit"><i class="icon-edit"></i> Edit</a> + <?php + } ?> + + <?php + if($ticket->isOpen() && !$ticket->isAssigned() && $thisstaff->canAssignTickets()) {?> + <a id="ticket-claim" class="action-button" href="#claim"><i class="icon-user"></i> Claim</a> + + <?php + }?> + + <a id="ticket-print" class="action-button" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=print"><i class="icon-print"></i> Print</a> + + <div id="action-dropdown-more" class="action-dropdown anchor-right"> + <ul> + <?php + if($ticket->isOpen() && ($dept && $dept->isManager($thisstaff))) { + + if($ticket->isAssigned()) { ?> + <li><a id="ticket-release" href="#release"><i class="icon-user"></i> Release (unassign) Ticket</a></li> + <?php + } + + if(!$ticket->isOverdue()) { ?> + <li><a id="ticket-overdue" href="#overdue"><i class="icon-bell"></i> Mark as Overdue</a></li> + <?php + } + + if($ticket->isAnswered()) { ?> + <li><a id="ticket-unanswered" href="#unanswered"><i class="icon-circle-arrow-left"></i> Mark as Unanswered</a></li> + <?php + } else { ?> + <li><a id="ticket-answered" href="#answered"><i class="icon-circle-arrow-right"></i> Mark as Answered</a></li> + <?php + } + } + + if($thisstaff->canBanEmails()) { + if(!$emailBanned) {?> + <li><a id="ticket-banemail" href="#banemail"><i class="icon-ban-circle"></i> Ban Email (<?php echo $ticket->getEmail(); ?>)</a></li> + <?php + } elseif($unbannable) { ?> + <li><a id="ticket-banemail" href="#unbanemail"><i class="icon-undo"></i> Unban Email (<?php echo $ticket->getEmail(); ?>)</a></li> + <?php + } + }?> + </ul> + </div> + </td> + </tr> +</table> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> + <tr> + <td width="50"> + <table border="0" cellspacing="" cellpadding="4" width="100%"> + <tr> + <th width="100">Status:</th> + <td><?php echo ucfirst($ticket->getStatus()); ?></td> + </tr> + <tr> + <th>Priority:</th> + <td><?php echo $ticket->getPriority(); ?></td> + </tr> + <tr> + <th>Department:</th> + <td><?php echo Format::htmlchars($ticket->getDeptName()); ?></td> + </tr> + <tr> + <th>Create Date:</th> + <td><?php echo Format::db_datetime($ticket->getCreateDate()); ?></td> + </tr> + </table> + </td> + <td width="50%"> + <table border="0" cellspacing="" cellpadding="4" width="100%"> + <tr> + <th width="100">Name:</th> + <td><?php echo Format::htmlchars($ticket->getName()); ?></td> + </tr> + <tr> + <th>Email:</th> + <td> + <?php + echo $ticket->getEmail(); + if(($related=$ticket->getRelatedTicketsCount())) { + echo sprintf(' <a href="tickets.php?a=search&query=%s" title="Related Tickets">(<b>%d</b>)</a>', + urlencode($ticket->getEmail()),$related); + + } + ?> + </td> + </tr> + <tr> + <th>Phone:</th> + <td><?php echo $ticket->getPhoneNumber(); ?></td> + </tr> + <tr> + <th>Source:</th> + <td><?php + echo Format::htmlchars($ticket->getSource()); + + if($ticket->getIP()) + echo ' <span class="faded">('.$ticket->getIP().')</span>'; + + + ?> + </td> + </tr> + </table> + </td> + </tr> +</table> +<br> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> + <tr> + <td width="50%"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <?php + if($ticket->isOpen()) { ?> + <tr> + <th width="100">Assigned To:</th> + <td> + <?php + if($ticket->isAssigned()) + echo Format::htmlchars(implode('/', $ticket->getAssignees())); + else + echo '<span class="faded">— Unassigned —</span>'; + ?> + </td> + </tr> + <?php + } else { ?> + <tr> + <th width="100">Closed By:</th> + <td> + <?php + if(($staff = $ticket->getStaff())) + echo Format::htmlchars($staff->getName()); + else + echo '<span class="faded">— Unknown —</span>'; + ?> + </td> + </tr> + <?php + } ?> + <tr> + <th>SLA Plan:</th> + <td><?php echo $sla?Format::htmlchars($sla->getName()):'<span class="faded">— none —</span>'; ?></td> + </tr> + <?php + if($ticket->isOpen()){ ?> + <tr> + <th>Due Date:</th> + <td><?php echo Format::db_datetime($ticket->getDueDate()); ?></td> + </tr> + <?php + }else { ?> + <tr> + <th>Close Date:</th> + <td><?php echo Format::db_datetime($ticket->getCloseDate()); ?></td> + </tr> + <?php + } + ?> + </table> + </td> + <td width="50%"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <tr> + <th width="100">Help Topic:</th> + <td><?php echo Format::htmlchars($ticket->getHelpTopic()); ?></td> + </tr> + <tr> + <th nowrap>Last Message:</th> + <td><?php echo Format::db_datetime($ticket->getLastMsgDate()); ?></td> + </tr> + <tr> + <th nowrap>Last Response:</th> + <td><?php echo Format::db_datetime($ticket->getLastRespDate()); ?></td> + </tr> + </table> + </td> + </tr> +</table> +<div class="clear"></div> +<h2 style="padding:10px 0 5px 0; font-size:11pt;"><?php echo Format::htmlchars($ticket->getSubject()); ?></h2> +<?php +$tcount = $ticket->getThreadCount(); +if($cfg->showNotesInline()) + $tcount+= $ticket->getNumNotes(); +?> +<ul id="threads"> + <li><a class="active" id="toggle_ticket_thread" href="#">Ticket Thread (<?php echo $tcount; ?>)</a></li> + <?php + if(!$cfg->showNotesInline()) {?> + <li><a id="toggle_notes" href="#">Internal Notes (<?php echo $ticket->getNumNotes(); ?>)</a></li> + <?php + }?> +</ul> +<?php +if(!$cfg->showNotesInline()) { ?> +<div id="ticket_notes"> + <?php + /* Internal Notes */ + if($ticket->getNumNotes() && ($notes=$ticket->getNotes())) { + foreach($notes as $note) { + + ?> + <table class="note" cellspacing="0" cellpadding="1" width="940" border="0"> + <tr> + <th width="640"> + <?php + echo sprintf('%s <em>posted by <b>%s</b></em>', + Format::htmlchars($note['title']), + Format::htmlchars($note['poster'])); + ?> + </th> + <th class="date" width="300"><?php echo Format::db_datetime($note['created']); ?></th> + </tr> + <tr> + <td colspan="2"> + <?php echo Format::display($note['body']); ?> + </td> + </tr> + <?php + if($note['attachments'] && ($links=$ticket->getAttachmentsLinks($note['id'],'N'))) {?> + <tr> + <td class="info" colspan="2"><?php echo $links; ?></td> + </tr> + <?php + }?> + </table> + <?php + } + } else { + echo "<p>No internal notes found.</p>"; + }?> +</div> +<?php +} ?> +<div id="ticket_thread"> + <?php + $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note'); + /* -------- Messages & Responses & Notes (if inline)-------------*/ + if(($thread=$ticket->getThread($cfg->showNotesInline()))) { + foreach($thread as $entry) { + ?> + <table class="<?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> + <tr> + <th width="200"><?php echo Format::db_datetime($entry['created']);?></th> + <th width="440"><span><?php echo Format::htmlchars($entry['title']); ?></span></th> + <th width="300" class="tmeta"><?php echo Format::htmlchars($entry['poster']); ?></th> + </tr> + <tr><td colspan=3><?php echo Format::display($entry['body']); ?></td></tr> + <?php + if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) {?> + <tr> + <td class="info" colspan=3><?php echo $links; ?></td> + </tr> + <?php + }?> + </table> + <?php + if($entry['thread_type']=='M') + $msgId=$entry['id']; + } + } else { + echo '<p>Error fetching ticket thread - get technical help.</p>'; + }?> +</div> +<div class="clear" style="padding-bottom:10px;"></div> +<?php if($errors['err']) { ?> + <div id="msg_error"><?php echo $errors['err']; ?></div> +<?php }elseif($msg) { ?> + <div id="msg_notice"><?php echo $msg; ?></div> +<?php }elseif($warn) { ?> + <div id="msg_warning"><?php echo $warn; ?></div> +<?php } ?> + +<div id="response_options"> + <ul> + <li><a id="reply_tab" href="#reply">Post Reply</a></li> + <li><a id="note_tab" href="#note">Post Internal Note</a></li> + <?php + if($thisstaff->canTransferTickets()) { ?> + <li><a id="transfer_tab" href="#transfer">Dept. Transfer</a></li> + <?php + } + + if($thisstaff->canAssignTickets()) { ?> + <li><a id="assign_tab" href="#assign"><?php echo $ticket->isAssigned()?'Reassign Ticket':'Assign Ticket'; ?></a></li> + <?php + } ?> + </ul> + + <form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="msgId" value="<?php echo $msgId; ?>"> + <input type="hidden" name="locktime" value="<?php echo $cfg->getLockTime(); ?>"> + <input type="hidden" name="a" value="reply"> + <span class="error"></span> + <table border="0" cellspacing="0" cellpadding="3"> + <tr> + <td width="160"> + <label><strong>TO:</strong></label> + </td> + <td width="765"> + <?php + $to = $ticket->getEmail(); + if(($name=$ticket->getName()) && !strpos($name,'@')) + $to =sprintf('%s <em><%s></em>', $name, $ticket->getEmail()); + echo $to; + ?> + + <label><input type='checkbox' value='1' name="emailreply" id="remailreply" + <?php echo ((!$info['emailreply'] && !$errors) || isset($info['emailreply']))?'checked="checked"':''; ?>> Email Reply</label> + </td> + </tr> + <?php + if($errors['response']) {?> + <tr><td width="160"> </td><td class="error"><?php echo $errors['response']; ?> </td></tr> + <?php + }?> + <tr> + <td width="160"> + <label><strong>Response:</strong></label> + </td> + <td width="765"> + <?php + if(($cannedResponses=Canned::responsesByDeptId($ticket->getDeptId()))) {?> + <select id="cannedResp" name="cannedResp"> + <option value="0" selected="selected">Select a canned response</option> + <?php + foreach($cannedResponses as $id =>$title) { + echo sprintf('<option value="%d">%s</option>',$id,$title); + } + ?> + </select> + + <label><input type='checkbox' value='1' name="append" id="append" checked="checked"> Append</label> + <br> + <?php + }?> + <textarea name="response" id="response" cols="50" rows="9" wrap="soft"><?php echo $info['response']; ?></textarea> + </td> + </tr> + <?php + if($cfg->allowAttachments()) { ?> + <tr> + <td width="160"> + <label for="attachment">Attachments:</label> + </td> + <td width="765" id="reply_form_attachments" class="attachments"> + <div class="canned_attachments"> + </div> + <div class="uploads"> + </div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + </td> + </tr> + <?php + }?> + <tr> + <td width="160"> + <label for="signature" class="left">Signature:</label> + </td> + <td width="765"> + <?php + $info['signature']=$info['signature']?$info['signature']:$thisstaff->getDefaultSignatureType(); + ?> + <label><input type="radio" name="signature" value="none" checked="checked"> None</label> + <?php + if($thisstaff->getSignature()) {?> + <label><input type="radio" name="signature" value="mine" + <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> My signature</label> + <?php + } ?> + <?php + if($dept && $dept->canAppendSignature()) { ?> + <label><input type="radio" name="signature" value="dept" + <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>> + Dept. Signature (<?php echo Format::htmlchars($dept->getName()); ?>)</label> + <?php + } ?> + </td> + </tr> + <?php + if($ticket->isClosed() || $thisstaff->canCloseTickets()) { ?> + <tr> + <td width="160"> + <label><strong>Ticket Status:</strong></label> + </td> + <td width="765"> + <?php + $statusChecked=isset($info['reply_ticket_status'])?'checked="checked"':''; + if($ticket->isClosed()) { ?> + <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Open" + <?php echo $statusChecked; ?>> Reopen on Reply</label> + <?php + } elseif($thisstaff->canCloseTickets()) { ?> + <label><input type="checkbox" name="reply_ticket_status" id="reply_ticket_status" value="Closed" + <?php echo $statusChecked; ?>> Close on Reply</label> + <?php + } ?> + </td> + </tr> + <?php + } ?> + </div> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Post Reply"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <form id="note" action="tickets.php?id=<?php echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="postnote"> + <table border="0" cellspacing="0" cellpadding="3"> + <?php + if($errors['note']) {?> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['postnote']; ?></td> + </tr> + <?php + } ?> + <tr> + <td width="160"> + <label><strong>Internal Note:</strong></label> + </td> + <td width="765"> + <div><span class="faded">Note details</span> + <span class="error">* <?php echo $errors['note']; ?></span></div> + <textarea name="note" id="internal_note" cols="80" rows="9" wrap="soft"><?php echo $info['note']; ?></textarea><br> + <div> + <span class="faded">Note title - sumarry of the note (optional)</span> + <span class="error" <?php echo $errors['title']; ?></span> + </div> + <input type="text" name="title" id="title" size="60" value="<?php echo $info['title']; ?>" > + </td> + </tr> + <?php + if($cfg->allowAttachments()) { ?> + <tr> + <td width="160"> + <label for="attachment">Attachments:</label> + </td> + <td width="765" class="attachments"> + <div class="uploads"> + </div> + <div class="file_input"> + <input type="file" class="multifile" name="attachments[]" size="30" value="" /> + </div> + </td> + </tr> + <?php + } + ?> + <tr><td colspan="2"> </td></tr> + <tr> + <td width="160"> + <label>Ticket Status:</label> + </td> + <td width="765"> + <div class="faded"></div> + <select name="state"> + <option value="" selected="selected">— unchanged —</option> + <?php + $state = $info['state']; + if($ticket->isClosed()){ + echo sprintf('<option value="open" %s>Reopen Ticket</option>', + ($state=='reopen')?'selected="selelected"':''); + } else { + if($thisstaff->canCloseTickets()) + echo sprintf('<option value="closed" %s>Close Ticket</option>', + ($state=='closed')?'selected="selelected"':''); + + /* Ticket open - states */ + echo '<option value="" disabled="disabled">— Ticket States —</option>'; + + //Answer - state + if($ticket->isAnswered()) + echo sprintf('<option value="unanswered" %s>Mark As Unanswered</option>', + ($state=='unanswered')?'selected="selelected"':''); + else + echo sprintf('<option value="answered" %s>Mark As Answered</option>', + ($state=='answered')?'selected="selelected"':''); + + //overdue - state + // Only department manager can set/clear overdue flag directly. + // Staff with edit perm. can still set overdue date & change SLA. + if($dept && $dept->isManager($thisstaff)) { + if(!$ticket->isOverdue()) + echo sprintf('<option value="overdue" %s>Flag As Overdue</option>', + ($state=='answered')?'selected="selelected"':''); + else + echo sprintf('<option value="notdue" %s>Clear Overdue Flag</option>', + ($state=='notdue')?'selected="selelected"':''); + + if($ticket->isAssigned()) + echo sprintf('<option value="unassigned" %s>Release (Unassign) Ticket</option>', + ($state=='unassigned')?'selected="selelected"':''); + } + }?> + </select> + <span class='error'>* <?php echo $errors['state']; ?></span> + </td> + </tr> + </div> + </table> + + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Post Note"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + if($thisstaff->canTransferTickets()) { ?> + <form id="transfer" action="tickets.php?id=<?php echo $ticket->getId(); ?>#transfer" name="transfer" method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="ticket_id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="transfer"> + <table border="0" cellspacing="0" cellpadding="3"> + <?php + if($errors['transfer']) { + ?> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['transfer']; ?></td> + </tr> + <?php + } ?> + <tr> + <td width="160"> + <label for="deptId"><strong>Department:</strong></label> + </td> + <td width="765"> + <?php + echo sprintf('<span class="faded">Ticket is currently in <b>%s</b> department.</span>', $ticket->getDeptName()); + ?> + <br> + <select id="deptId" name="deptId"> + <option value="0" selected="selected">— Select Target Department —</option> + <?php + if($depts=Dept::getDepartments()) { + foreach($depts as $id =>$name) { + if($id==$ticket->getDeptId()) continue; + echo sprintf('<option value="%d" %s>%s</option>', + $id, ($info['deptId']==$id)?'selected="selected"':'',$name); + } + } + ?> + </select> <span class='error'>* <?php echo $errors['deptId']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + <label><strong>Comments:</strong></label> + </td> + <td width="765"> + <span class="faded">Enter reasons for the transfer.</span> + <span class="error">* <?php echo $errors['transfer_comments']; ?></span><br> + <textarea name="transfer_comments" id="transfer_comments" + cols="80" rows="7" wrap="soft"><?php echo $info['transfer_comments']; ?></textarea> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="Transfer"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + } ?> + <?php + if($thisstaff->canAssignTickets()) { ?> + <form id="assign" action="tickets.php?id=<?php echo $ticket->getId(); ?>#assign" name="assign" method="post" enctype="multipart/form-data"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="assign"> + <table border="0" cellspacing="0" cellpadding="3"> + + <?php + if($errors['assign']) { + ?> + <tr> + <td width="160"> </td> + <td class="error"><?php echo $errors['assign']; ?></td> + </tr> + <?php + } ?> + <tr> + <td width="160"> + <label for="assignId"><strong>Assignee:</strong></label> + </td> + <td width="765"> + <?php + if($ticket->isAssigned() && $ticket->isOpen()) { + echo sprintf('<span class="faded">Ticket is currently assigned to <b>%s</b></span>', + $ticket->getAssignee()); + } else { + echo '<span class="faded">Assigning a closed ticket will <b>reopen</b> it!</span>'; + } + ?> + <br> + <select id="assignId" name="assignId"> + <option value="0" selected="selected">— Select Staff Member OR a Team —</option> + <?php + if($ticket->isOpen() && !$ticket->isAssigned()) + echo sprintf('<option value="%d">Claim Ticket (comments optional)</option>', $thisstaff->getId()); + + $sid=$tid=0; + if(($users=Staff::getAvailableStaffMembers())) { + echo '<OPTGROUP label="Staff Members ('.count($users).')">'; + $staffId=$ticket->isAssigned()?$ticket->getStaffId():0; + foreach($users as $id => $name) { + if($staffId && $staffId==$id) + continue; + + $k="s$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + + if(($teams=Team::getActiveTeams())) { + echo '<OPTGROUP label="Teams ('.count($teams).')">'; + $teamId=(!$sid && $ticket->isAssigned())?$ticket->getTeamId():0; + foreach($teams as $id => $name) { + if($teamId && $teamId==$id) + continue; + + $k="t$id"; + echo sprintf('<option value="%s" %s>%s</option>', + $k,(($info['assignId']==$k)?'selected="selected"':''),$name); + } + echo '</OPTGROUP>'; + } + ?> + </select> <span class='error'>* <?php echo $errors['assignId']; ?></span> + </td> + </tr> + <tr> + <td width="160"> + <label><strong>Comments:</strong><span class='error'> </span></label> + </td> + <td width="765"> + <span class="faded">Enter reasons for the assignment or instructions for assignee.</span> + <span class="error">* <?php echo $errors['assign_comments']; ?></span><br> + <textarea name="assign_comments" id="assign_comments" cols="80" rows="7" wrap="soft"><?php echo $info['assign_comments']; ?></textarea> + </td> + </tr> + </table> + <p style="padding-left:165px;"> + <input class="btn_sm" type="submit" value="<?php echo $ticket->isAssigned()?'Reassign':'Assign'; ?>"> + <input class="btn_sm" type="reset" value="Reset"> + </p> + </form> + <?php + } ?> +</div> +<div style="display:none;" class="dialog" id="print-options"> + <h3>Ticket Print Options</h3> + <a class="close" href="">×</a> + <hr/> + <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post" id="print-form" name="print-form"> + <?php csrf_token(); ?> + <input type="hidden" name="a" value="print"> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <fieldset class="notes"> + <label for="notes">Print Notes:</label> + <input type="checkbox" id="notes" name="notes" value="1"> Print <b>Internal</b> Notes/Comments + </fieldset> + <fieldset> + <label for="psize">Paper Size:</label> + <select id="psize" name="psize"> + <option value="">— Select Print Paper Size —</option> + <?php + $options=array('Letter', 'Legal', 'A4', 'A3'); + $psize =$_SESSION['PAPER_SIZE']?$_SESSION['PAPER_SIZE']:$thisstaff->getDefaultPaperSize(); + foreach($options as $v) { + echo sprintf('<option value="%s" %s>%s</option>', + $v,($psize==$v)?'selected="selected"':'', $v); + } + ?> + </select> + </fieldset> + <hr style="margin-top:3em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Print"> + </span> + </p> + </form> + <div class="clear"></div> +</div> +<div style="display:none;" class="dialog" id="ticket-status"> + <h3><?php echo sprintf('%s Ticket #%s', ($ticket->isClosed()?'Reopen':'Close'), $ticket->getNumber()); ?></h3> + <a class="close" href="">×</a> + <hr/> + <?php echo sprintf('Are you sure you want to <b>%s</b> this ticket?', $ticket->isClosed()?'REOPEN':'CLOSE'); ?> + <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post" id="status-form" name="status-form"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="process"> + <input type="hidden" name="do" value="<?php echo $ticket->isClosed()?'reopen':'close'; ?>"> + <fieldset> + <em>Reasons for status change (internal note). Optional but highly recommended.</em><br> + <textarea name="ticket_status_notes" id="ticket_status_notes" cols="50" rows="5" wrap="soft"><?php echo $info['ticket_status_notes']; ?></textarea> + </fieldset> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="<?php echo $ticket->isClosed()?'Reopen':'Close'; ?>"> + </span> + </p> + </form> + <div class="clear"></div> +</div> +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="claim-confirm"> + Are you sure want to <b>claim</b> (self assign) this ticket? + </p> + <p class="confirm-action" style="display:none;" id="answered-confirm"> + Are you sure want to flag the ticket as <b>answered</b>? + </p> + <p class="confirm-action" style="display:none;" id="unanswered-confirm"> + Are you sure want to flag the ticket as <b>unanswered</b>? + </p> + <p class="confirm-action" style="display:none;" id="overdue-confirm"> + Are you sure want to flag the ticket as <font color="red"><b>overdue</b></font>? + </p> + <p class="confirm-action" style="display:none;" id="banemail-confirm"> + Are you sure want to <b>ban</b> <?php echo $ticket->getEmail(); ?>? <br><br> + New tickets from the email address will be auto-rejected. + </p> + <p class="confirm-action" style="display:none;" id="unbanemail-confirm"> + Are you sure want to <b>remove</b> <?php echo $ticket->getEmail(); ?> from ban list? + </p> + <p class="confirm-action" style="display:none;" id="release-confirm"> + Are you sure want to <b>unassign</b> ticket from <b><?php echo $ticket->getAssigned(); ?></b>? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE this ticket?</strong></font> + <br><br>Deleted tickets CANNOT be recovered, including any associated attachments. + </p> + <div>Please confirm to continue.</div> + <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post" id="confirm-form" name="confirm-form"> + <?php csrf_token(); ?> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> + <input type="hidden" name="a" value="process"> + <input type="hidden" name="do" id="action" value=""> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="OK"> + </span> + </p> + </form> + <div class="clear"></div> +</div> +<script type="text/javascript" src="js/ticket.js"></script> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index cbe8a6a982c22ee6c5c2ecfc2427164f9254e65d..534a358bfcc01bbb771340f78db2b5fcd7924cbd 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -193,15 +193,27 @@ $orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); $order_by=$order=null; if($_REQUEST['sort'] && $sortOptions[$_REQUEST['sort']]) $order_by =$sortOptions[$_REQUEST['sort']]; +elseif(!strcasecmp($status, 'open') && !$showanswered && $sortOptions[$_SESSION['tickets']['sort']]) { + $_REQUEST['sort'] = $_SESSION['tickets']['sort']; + $order_by = $sortOptions[$_SESSION['tickets']['sort']]; + $order = $_SESSION['tickets']['order']; +} if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) $order=$orderWays[strtoupper($_REQUEST['order'])]; +//Save sort order for sticky sorting. +if(!strcasecmp($status, 'open') && $_REQUEST['sort']) { + $_SESSION['tickets']['sort'] = $_REQUEST['sort']; + $_SESSION['tickets']['order'] = $_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:'ASC'; @@ -285,10 +297,11 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <!-- SEARCH FORM END --> <div class="clear"></div> <div style="margin-bottom:20px"> -<form action="tickets.php" method="POST" name='tickets' onSubmit="return checkbox_checker(this,1,0);"> +<form action="tickets.php" method="POST" name='tickets'> <?php csrf_token(); ?> <a class="refresh" href="<?php echo $_SERVER['REQUEST_URI']; ?>">Refresh</a> <input type="hidden" name="a" value="mass_process" > + <input type="hidden" name="do" id="action" value="" > <input type="hidden" name="status" value="<?php echo $status; ?>" > <table class="list" border="0" cellspacing="1" cellpadding="2" width="940"> <caption><?php echo $showing; ?> <?php echo $results_type; ?></caption> @@ -345,6 +358,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. $class = "row1"; $total=0; if($res && ($num=db_num_rows($res))): + $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null; while ($row = db_fetch_array($res)) { $tag=$row['staff_id']?'assigned':'openticket'; $flag=null; @@ -372,9 +386,14 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. } ?> <tr id="<?php echo $row['ticket_id']; ?>"> - <?php if($thisstaff->canManageTickets()) { ?> + <?php if($thisstaff->canManageTickets()) { + + $sel=false; + if($ids && in_array($row['ticket_id'], $ids)) + $sel=true; + ?> <td align="center" class="nohover"> - <input type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" onClick="highLight(this.value,this.checked);"> + <input class="ckb" type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>> </td> <?php } ?> <td align="center" title="<?php echo $row['email']; ?>" nowrap> @@ -411,11 +430,11 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <tfoot> <tr> <td colspan="7"> - <?php if($res && $num){ ?> + <?php if($res && $num && $thisstaff->canManageTickets()){ ?> Select: - <a href="#" onclick="return select_all(document.forms['tickets'],true)">All</a> - <a href="#" onclick="return reset_all(document.forms['tickets'])">None</a> - <a href="#" onclick="return toogle_all(document.forms['tickets'],true)">Toggle</a> + <a id="selectAll" href="#ckb">All</a> + <a id="selectNone" href="#ckb">None</a> + <a id="selectToggle" href="#ckb">Toggle</a> <?php }else{ echo '<i>'; echo $ferror?Format::htmlchars($ferror):'Query returned 0 results.'; @@ -433,36 +452,30 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. ?> <?php if($thisstaff->canManageTickets()) { ?> - <p class="centered"> + <p class="centered" id="actions"> <?php $status=$_REQUEST['status']?$_REQUEST['status']:$status; switch (strtolower($status)) { case 'closed': ?> - <input class="button" type="submit" name="reopen" value="Reopen" - onClick=' return confirm("Are you sure you want to reopen selected tickets?");'> + <input class="button" type="submit" name="reopen" value="Reopen" > <?php break; case 'open': case 'answered': case 'assigned': ?> - <input class="button" type="submit" name="overdue" value="Overdue" - onClick=' return confirm("Are you sure you want to mark selected tickets overdue/stale?");'> - <input class="button" type="submit" name="close" value="Close" - onClick=' return confirm("Are you sure you want to close selected tickets?");'> + <input class="button" type="submit" name="mark_overdue" value="Overdue" > + <input class="button" type="submit" name="close" value="Close"> <?php break; default: //search?? ?> - <input class="button" type="submit" name="close" value="Close" - onClick=' return confirm("Are you sure you want to close selected tickets?");'> - <input class="button" type="submit" name="reopen" value="Reopen" - onClick=' return confirm("Are you sure you want to reopen selected tickets?");'> + <input class="button" type="submit" name="close" value="Close" > + <input class="button" type="submit" name="reopen" value="Reopen"> <?php } if($thisstaff->canDeleteTickets()) { ?> - <input class="button" type="submit" name="delete" value="Delete" - onClick=' return confirm("Are you sure you want to DELETE selected tickets?");'> + <input class="button" type="submit" name="delete" value="Delete"> <?php } ?> </p> <?php @@ -470,8 +483,38 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. } ?> </form> </div> -<div id="search_overlay"></div> -<div style="display:none;" id="advanced-search"> + +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" style="display:none;" id="close-confirm"> + Are you sure want to <b>close</b> selected open tickets? + </p> + <p class="confirm-action" style="display:none;" id="reopen-confirm"> + Are you sure want to <b>reopen</b> selected closed tickets? + </p> + <p class="confirm-action" style="display:none;" id="mark_overdue-confirm"> + Are you sure want to flag the selected tickets as <font color="red"><b>overdue</b></font>? + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected tickets?</strong></font> + <br><br>Deleted tickets CANNOT be recovered, including any associated attachments. + </p> + <div>Please confirm to continue.</div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="button" value="No, Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="button" value="Yes, Do it!" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> + +<div class="dialog" style="display:none;" id="advanced-search"> <h3>Advanced Ticket Search</h3> <a class="close" href="">×</a> <form action="tickets.php" method="post" id="search" name="search"> diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php index c973ab9ff9c1d083e6a6a13643e8d49ea14ff16b..13dcf25718874fb7eb48371ad68daba9ff078312 100644 --- a/include/staff/tpl.inc.php +++ b/include/staff/tpl.inc.php @@ -12,7 +12,7 @@ $info=array_merge($template->getMsgTemplate($info['tpl']),$info); <input type="hidden" name="id" value="<?php echo $template->getId(); ?>"> <input type="hidden" name="a" value="manage"> Message Template: - <select name="tpl" style="width:300px;"> + <select id="tpl_options" name="tpl" style="width:300px;"> <option value="">— Select Setting Group —</option> <?php foreach($msgtemplates as $k=>$v) { diff --git a/include/upgrader/sql/15719536-dd0022fb.patch.sql b/include/upgrader/sql/15719536-dd0022fb.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..5a44aa898bb9e88ca5c6cda017910f05bbb0e206 --- /dev/null +++ b/include/upgrader/sql/15719536-dd0022fb.patch.sql @@ -0,0 +1,11 @@ + +/** + * @version v1.7 RC2+ + * @signature dd0022fb14892c0bb6a9700392df2de7 + * + * Transitional patch - dev branch installations + * + */ + +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='dd0022fb14892c0bb6a9700392df2de7'; diff --git a/include/upgrader/sql/15b30765-dd0022fb.cleanup.sql b/include/upgrader/sql/15b30765-dd0022fb.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..fdbd27d1f799d5da54b12d60522aa451f177798c --- /dev/null +++ b/include/upgrader/sql/15b30765-dd0022fb.cleanup.sql @@ -0,0 +1,10 @@ + +-- Drop fields we no longer need in the reference table. +-- NOTE: This was moved from the 1.6* major upgrade script because the +-- handling of attachments changed with dd0022fb +ALTER TABLE `%TABLE_PREFIX%ticket_attachment` + DROP `file_size`, + DROP `file_name`, + DROP `file_key`, + DROP `updated`, + DROP `deleted`; diff --git a/include/upgrader/sql/15b30765-dd0022fb.patch.sql b/include/upgrader/sql/15b30765-dd0022fb.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..0006139d679fdf9af8924b639b488e3edc0f2c34 --- /dev/null +++ b/include/upgrader/sql/15b30765-dd0022fb.patch.sql @@ -0,0 +1,26 @@ +/** + * @version v1.7 RC2+ + * @signature dd0022fb14892c0bb6a9700392df2de7 + * + * Migrate file attachment data from %file to %file_chunk + * + */ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`; +CREATE TABLE `%TABLE_PREFIX%file_chunk` ( + `file_id` int(11) NOT NULL, + `chunk_id` int(11) NOT NULL, + `filedata` longblob NOT NULL, + PRIMARY KEY (`file_id`, `chunk_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`) + SELECT `id`, 0, `filedata` + FROM `%TABLE_PREFIX%file`; + +ALTER TABLE `%TABLE_PREFIX%file` DROP COLUMN `filedata`; +OPTIMIZE TABLE `%TABLE_PREFIX%file`; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='dd0022fb14892c0bb6a9700392df2de7'; diff --git a/include/upgrader/sql/1da1bcba-15b30765.patch.sql b/include/upgrader/sql/1da1bcba-15b30765.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..cb37a02f4af2e3c86f0367674454b22554973c3b --- /dev/null +++ b/include/upgrader/sql/1da1bcba-15b30765.patch.sql @@ -0,0 +1,11 @@ +/** + * @version v1.7 RC2+ + * @signature 15b3076533123ff617801d89861136c8 + * + * Transitional patch. + * + */ + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='15b3076533123ff617801d89861136c8'; diff --git a/include/upgrader/sql/c00511c7-7be60a84.cleanup.sql b/include/upgrader/sql/c00511c7-7be60a84.cleanup.sql index 01d69e2d72b732b2d7c3f618fd6e595ccb7c67f7..eabc72c9e22d724978f4f5449901ea5157b2b6a1 100644 --- a/include/upgrader/sql/c00511c7-7be60a84.cleanup.sql +++ b/include/upgrader/sql/c00511c7-7be60a84.cleanup.sql @@ -4,14 +4,6 @@ ALTER TABLE `%TABLE_PREFIX%config` DROP COLUMN `timezone_offset`, DROP COLUMN `api_passphrase`; --- 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 staff table. ALTER TABLE `%TABLE_PREFIX%staff` DROP `append_signature`, diff --git a/include/upgrader/sql/c00511c7-7be60a84.patch.sql b/include/upgrader/sql/c00511c7-7be60a84.patch.sql index 638248b976019a670c773578b564d99a3daacc6a..80e652cc7a91d3cc7a90f18c4059343651d49099 100644 --- a/include/upgrader/sql/c00511c7-7be60a84.patch.sql +++ b/include/upgrader/sql/c00511c7-7be60a84.patch.sql @@ -63,11 +63,15 @@ ALTER TABLE `%TABLE_PREFIX%config` 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_kb` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' 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`; +-- copy over timezone id - based on offset. +UPDATE `%TABLE_PREFIX%config` SET default_timezone_id = + (SELECT id FROM `%TABLE_PREFIX%timezone` WHERE offset = `%TABLE_PREFIX%config`.timezone_offset); + ALTER TABLE `%TABLE_PREFIX%staff` ADD `passwdreset` DATETIME NULL DEFAULT NULL AFTER `lastlogin`; @@ -145,8 +149,8 @@ ALTER TABLE `%TABLE_PREFIX%email_template` 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.'; +-- Insert default text for the new messaage tpl + make templates active (all records are updated). +UPDATE `%TABLE_PREFIX%email_template` SET updated=NOW() ,isactive=1, 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, diff --git a/include/upgrader/sql/d0e37dca-1da1bcba.patch.sql b/include/upgrader/sql/d0e37dca-1da1bcba.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..3d3bda68e2fd49293e1a73545506416c7a1cc126 --- /dev/null +++ b/include/upgrader/sql/d0e37dca-1da1bcba.patch.sql @@ -0,0 +1,23 @@ +/** + * @version v1.7 RC3 + * @signature 1da1bcbafcedc65efef58f142a48ac91 + * + * Upgrade from 1.6 RC3 + filters + * + */ + +RENAME TABLE `%TABLE_PREFIX%email_filter` TO `%TABLE_PREFIX%filter`; + +RENAME TABLE `%TABLE_PREFIX%email_filter_rule` TO `%TABLE_PREFIX%filter_rule`; + +ALTER TABLE `%TABLE_PREFIX%filter` CHANGE `reject_email` `reject_ticket` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0'; + +ALTER TABLE `%TABLE_PREFIX%filter` + ADD `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any' AFTER `sla_id` , + ADD INDEX ( `target` ); + +UPDATE `%TABLE_PREFIX%filter` SET `target` = 'Email' WHERE `email_id` != 0; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='1da1bcbafcedc65efef58f142a48ac91'; diff --git a/include/upgrader/sql/dd0022fb-f4da0c9b.patch.sql b/include/upgrader/sql/dd0022fb-f4da0c9b.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..c0f10cfa52ec2a8767109d12cfcdec38d9d13136 --- /dev/null +++ b/include/upgrader/sql/dd0022fb-f4da0c9b.patch.sql @@ -0,0 +1,445 @@ +/* + * @version=1.7RC2+ + * + * change variable names + */ + +-- Canned Responses (with variables) +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%id', '%{ticket.id}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%ticket', '%{ticket.number}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%name', '%{ticket.name}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%email', '%{ticket.email}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%subject', '%{ticket.subject}'); + +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%status', '%{ticket.status}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%priority', '%{ticket.priority}'); + +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%auth', '%{ticket.auth_token}'); + +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%phone', '%{ticket.phone_number}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%createdate', '%{ticket.create_date}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%duedate', '%{ticket.due_date}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%closedate', '%{ticket.close_date}'); + +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%topic', '%{ticket.topic.name}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%dept', '%{ticket.dept.name}'); +UPDATE `%TABLE_PREFIX%canned_response` SET `response` = REPLACE(`response`, '%team', '%{ticket.team.name}'); + +-- %id +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%id', '%{ticket.id}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%id', '%{ticket.id}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%id', '%{ticket.id}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%id', '%{ticket.id}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%id', '%{ticket.id}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%id', '%{ticket.id}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%id', '%{ticket.id}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%id', '%{ticket.id}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%id', '%{ticket.id}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%id', '%{ticket.id}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%id', '%{ticket.id}'); + +-- %ticket +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%ticket', '%{ticket.number}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%ticket', '%{ticket.number}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%ticket', '%{ticket.number}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%ticket', '%{ticket.number}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%ticket', '%{ticket.number}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%ticket', '%{ticket.number}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%ticket', '%{ticket.number}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%ticket', '%{ticket.number}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%ticket', '%{ticket.number}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%ticket', '%{ticket.number}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%ticket', '%{ticket.number}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%ticket', '%{ticket.number}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%ticket', '%{ticket.number}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%ticket', '%{ticket.number}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%ticket', '%{ticket.number}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%ticket', '%{ticket.number}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%ticket', '%{ticket.number}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%ticket', '%{ticket.number}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%ticket', '%{ticket.number}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%ticket', '%{ticket.number}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%ticket', '%{ticket.number}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%ticket', '%{ticket.number}'); + +-- %subject +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%subject', '%{ticket.subject}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%subject', '%{ticket.subject}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%subject', '%{ticket.subject}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%subject', '%{ticket.subject}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%subject', '%{ticket.subject}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%subject', '%{ticket.subject}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%subject', '%{ticket.subject}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%subject', '%{ticket.subject}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%subject', '%{ticket.subject}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%subject', '%{ticket.subject}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%subject', '%{ticket.subject}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%subject', '%{ticket.subject}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%subject', '%{ticket.subject}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%subject', '%{ticket.subject}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%subject', '%{ticket.subject}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%subject', '%{ticket.subject}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%subject', '%{ticket.subject}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%subject', '%{ticket.subject}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%subject', '%{ticket.subject}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%subject', '%{ticket.subject}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%subject', '%{ticket.subject}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%subject', '%{ticket.subject}'); + +-- %name +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%name', '%{ticket.name}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%name', '%{ticket.name}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%name', '%{ticket.name}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%name', '%{ticket.name}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%name', '%{ticket.name}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%name', '%{ticket.name}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%name', '%{ticket.name}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%name', '%{ticket.name}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%name', '%{ticket.name}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%name', '%{ticket.name}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%name', '%{ticket.name}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%name', '%{ticket.name}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%name', '%{ticket.name}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%name', '%{ticket.name}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%name', '%{ticket.name}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%name', '%{ticket.name}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%name', '%{ticket.name}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%name', '%{ticket.name}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%name', '%{ticket.name}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%name', '%{ticket.name}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%name', '%{ticket.name}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%name', '%{ticket.name}'); + +-- %email +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%email', '%{ticket.email}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%email', '%{ticket.email}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%email', '%{ticket.email}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%email', '%{ticket.email}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%email', '%{ticket.email}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%email', '%{ticket.email}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%email', '%{ticket.email}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%email', '%{ticket.email}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%email', '%{ticket.email}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%email', '%{ticket.email}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%email', '%{ticket.email}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%email', '%{ticket.email}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%email', '%{ticket.email}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%email', '%{ticket.email}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%email', '%{ticket.email}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%email', '%{ticket.email}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%email', '%{ticket.email}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%email', '%{ticket.email}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%email', '%{ticket.email}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%email', '%{ticket.email}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%email', '%{ticket.email}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%email', '%{ticket.email}'); + +-- %status +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%status', '%{ticket.status}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%status', '%{ticket.status}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%status', '%{ticket.status}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%status', '%{ticket.status}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%status', '%{ticket.status}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%status', '%{ticket.status}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%status', '%{ticket.status}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%status', '%{ticket.status}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%status', '%{ticket.status}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%status', '%{ticket.status}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%status', '%{ticket.status}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%status', '%{ticket.status}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%status', '%{ticket.status}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%status', '%{ticket.status}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%status', '%{ticket.status}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%status', '%{ticket.status}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%status', '%{ticket.status}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%status', '%{ticket.status}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%status', '%{ticket.status}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%status', '%{ticket.status}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%status', '%{ticket.status}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%status', '%{ticket.status}'); + +-- %priority +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_subj` = REPLACE(`ticket_autoresp_subj`, '%priority', '%{ticket.priority}'), + `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%priority', '%{ticket.priority}'), + `message_autoresp_subj` = REPLACE(`message_autoresp_subj`, '%priority', '%{ticket.priority}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%priority', '%{ticket.priority}'), + `ticket_notice_subj` = REPLACE(`ticket_notice_subj`, '%priority', '%{ticket.priority}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%priority', '%{ticket.priority}'), + `ticket_overlimit_subj` = REPLACE(`ticket_overlimit_subj`, '%priority', '%{ticket.priority}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%priority', '%{ticket.priority}'), + `ticket_reply_subj` = REPLACE(`ticket_reply_subj`, '%priority', '%{ticket.priority}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%priority', '%{ticket.priority}'), + `ticket_alert_subj` = REPLACE(`ticket_alert_subj`, '%priority', '%{ticket.priority}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%priority', '%{ticket.priority}'), + `message_alert_subj` = REPLACE(`message_alert_subj`, '%priority', '%{ticket.priority}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%priority', '%{ticket.priority}'), + `note_alert_subj` = REPLACE(`note_alert_subj`, '%priority', '%{ticket.priority}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%priority', '%{ticket.priority}'), + `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%priority', '%{ticket.priority}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%priority', '%{ticket.priority}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%priority', '%{ticket.priority}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%priority', '%{ticket.priority}'), + `ticket_overdue_subj` = REPLACE(`ticket_overdue_subj`, '%priority', '%{ticket.priority}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%priority', '%{ticket.priority}'); + +-- %auth +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%auth', '%{ticket.auth_code}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%auth', '%{ticket.auth_code}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%auth', '%{ticket.auth_code}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%auth', '%{ticket.auth_code}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%auth', '%{ticket.auth_code}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%auth', '%{ticket.auth_code}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%auth', '%{ticket.auth_code}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%auth', '%{ticket.auth_code}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%auth', '%{ticket.auth_code}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%auth', '%{ticket.auth_code}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%auth', '%{ticket.auth_code}'); + +-- %phone +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%phone', '%{ticket.phone_number}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%phone', '%{ticket.phone_number}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%phone', '%{ticket.phone_number}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%phone', '%{ticket.phone_number}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%phone', '%{ticket.phone_number}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%phone', '%{ticket.phone_number}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%phone', '%{ticket.phone_number}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%phone', '%{ticket.phone_number}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%phone', '%{ticket.phone_number}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%phone', '%{ticket.phone_number}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%phone', '%{ticket.phone_number}'); + +-- %createdate +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%createdate', '%{ticket.create_date}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%createdate', '%{ticket.create_date}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%createdate', '%{ticket.create_date}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%createdate', '%{ticket.create_date}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%createdate', '%{ticket.create_date}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%createdate', '%{ticket.create_date}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%createdate', '%{ticket.create_date}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%createdate', '%{ticket.create_date}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%createdate', '%{ticket.create_date}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%createdate', '%{ticket.create_date}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%createdate', '%{ticket.create_date}'); + +-- %duedate +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%duedate', '%{ticket.due_date}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%duedate', '%{ticket.due_date}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%duedate', '%{ticket.due_date}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%duedate', '%{ticket.due_date}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%duedate', '%{ticket.due_date}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%duedate', '%{ticket.due_date}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%duedate', '%{ticket.due_date}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%duedate', '%{ticket.due_date}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%duedate', '%{ticket.due_date}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%duedate', '%{ticket.due_date}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%duedate', '%{ticket.due_date}'); + +-- %closedate +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%closedate', '%{ticket.close_date}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%closedate', '%{ticket.close_date}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%closedate', '%{ticket.close_date}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%closedate', '%{ticket.close_date}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%closedate', '%{ticket.close_date}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%closedate', '%{ticket.close_date}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%closedate', '%{ticket.close_date}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%closedate', '%{ticket.close_date}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%closedate', '%{ticket.close_date}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%closedate', '%{ticket.close_date}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%closedate', '%{ticket.close_date}'); + +-- %topic +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%topic', '%{ticket.topic.name}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%topic', '%{ticket.topic.name}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%topic', '%{ticket.topic.name}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%topic', '%{ticket.topic.name}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%topic', '%{ticket.topic.name}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%topic', '%{ticket.topic.name}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%topic', '%{ticket.topic.name}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%topic', '%{ticket.topic.name}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%topic', '%{ticket.topic.name}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%topic', '%{ticket.topic.name}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%topic', '%{ticket.topic.name}'); + +-- %dept +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%dept', '%{ticket.dept.name}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%dept', '%{ticket.dept.name}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%dept', '%{ticket.dept.name}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%dept', '%{ticket.dept.name}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%dept', '%{ticket.dept.name}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%dept', '%{ticket.dept.name}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%dept', '%{ticket.dept.name}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%dept', '%{ticket.dept.name}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%dept', '%{ticket.dept.name}'), + `transfer_alert_subj` = REPLACE(`transfer_alert_subj`, '%dept', '%{ticket.dept.name}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%dept', '%{ticket.dept.name}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%dept', '%{ticket.dept.name}'); + +-- %team +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%team', '%{ticket.team.name}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%team', '%{ticket.team.name}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%team', '%{ticket.team.name}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%team', '%{ticket.team.name}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%team', '%{ticket.team.name}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%team', '%{ticket.team.name}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%team', '%{ticket.team.name}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%team', '%{ticket.team.name}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%team', '%{ticket.team.name}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%team', '%{ticket.team.name}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%team', '%{ticket.team.name}'); + +-- %clientlink +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%clientlink', '%{ticket.client_link}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%clientlink', '%{ticket.client_link}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%clientlink', '%{ticket.client_link}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%clientlink', '%{ticket.client_link}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%clientlink', '%{ticket.client_link}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%clientlink', '%{ticket.client_link}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%clientlink', '%{ticket.client_link}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%clientlink', '%{ticket.client_link}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%clientlink', '%{ticket.client_link}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%clientlink', '%{ticket.client_link}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%clientlink', '%{ticket.client_link}'); + +-- %staff (recipient of the alert) +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%staff', '%{recipient}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%staff', '%{recipient}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%staff', '%{recipient}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%staff', '%{recipient}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%staff', '%{recipient}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%staff', '%{recipient}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%staff', '%{recipient}'); + +-- %message +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%message', '%{message}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%message', '%{message}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%message', '%{message}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%message', '%{message}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%message', '%{message}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%message', '%{message}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%message', '%{message}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%message', '%{message}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%message', '%{message}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%message', '%{message}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%message', '%{message}'); + +-- %response +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%response', '%{response}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%response', '%{response}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%response', '%{response}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%response', '%{response}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%response', '%{response}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%response', '%{response}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%response', '%{response}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%response', '%{response}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%response', '%{response}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%response', '%{response}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%response', '%{response}'); + +-- %note +UPDATE `%TABLE_PREFIX%email_template` + SET `note_alert_body` = REPLACE(`note_alert_body`, '%note', '* %{note.title} *\r\n\r\n%{note.message}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%note', '%{comments}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%note', '%{comments}'); + +-- %{note} (dev branch installations) +UPDATE `%TABLE_PREFIX%email_template` + SET `note_alert_body` = REPLACE(`note_alert_body`, '%{note}', '%{note.message}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%{note}', '%{comments}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%{note}', '%{comments}'); + +-- %{title} (dev branch installations) +UPDATE `%TABLE_PREFIX%email_template` + SET `note_alert_body` = REPLACE(`note_alert_body`, '%{title}', '* %{note.title} *\r\n'); + +-- %url +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%url', '%{url}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%url', '%{url}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%url', '%{url}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%url', '%{url}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%url', '%{url}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%url', '%{url}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%url', '%{url}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%url', '%{url}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%url', '%{url}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%url', '%{url}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%url', '%{url}'); + +-- %signature +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%signature', '%{signature}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%signature', '%{signature}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%signature', '%{signature}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%signature', '%{signature}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%signature', '%{signature}'), + `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%signature', '%{signature}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%signature', '%{signature}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%signature', '%{signature}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%signature', '%{signature}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%signature', '%{signature}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%signature', '%{signature}'); + +-- %assignee +UPDATE `%TABLE_PREFIX%email_template` + SET `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%assignee', '%{assignee}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%assignee', '%{assignee}'); + +-- %assigner +UPDATE `%TABLE_PREFIX%email_template` + SET `assigned_alert_subj` = REPLACE(`assigned_alert_subj`, '%assigner', '%{assigner}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%assigner', '%{assigner}'); + +/* Change links */ + +-- Client URL -> %{url}/view.php?e=%{ticket.email}&t=%{ticket.number} +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoresp_body` = REPLACE(`ticket_autoresp_body`, '%{url}/view.php?e=%{ticket.email}&t=%{ticket.number}', '%{ticket.client_link}'), + `message_autoresp_body` = REPLACE(`message_autoresp_body`, '%{url}/view.php?e=%{ticket.email}&t=%{ticket.number}', '%{ticket.client_link}'), + `ticket_notice_body` = REPLACE(`ticket_notice_body`, '%{url}/view.php?e=%{ticket.email}&t=%{ticket.number}', '%{ticket.client_link}'), + `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%{url}/view.php?e=%{ticket.email}&t=%{ticket.number}', '%{ticket.client_link}'), + `ticket_reply_body` = REPLACE(`ticket_reply_body`, '%{url}/view.php?e=%{ticket.email}&t=%{ticket.number}', '%{ticket.client_link}'); + +-- Client URL -> %{url}/view.php?e=%{ticket.email} (overlimit template) +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_overlimit_body` = REPLACE(`ticket_overlimit_body`, '%{url}/view.php?e=%{ticket.email}', '%{url}/tickets.php?e=%{ticket.email}'); + +-- Staff URL -> %{url}/scp/ticket.php?id=%{ticket.id} (should be tickets.php) +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%{url}/scp/ticket.php?id=%{ticket.id}', '%{ticket.staff_link}'); + +-- Staff URL 2 -> %{url}/scp/tickets.php?id=%{ticket.id} +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_alert_body` = REPLACE(`ticket_alert_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `message_alert_body` = REPLACE(`message_alert_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `note_alert_body` = REPLACE(`note_alert_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `transfer_alert_body` = REPLACE(`transfer_alert_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'), + `ticket_overdue_body` = REPLACE(`ticket_overdue_body`, '%{url}/scp/tickets.php?id=%{ticket.id}', '%{ticket.staff_link}'); + + -- update schema signature +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='f4da0c9befa257b5a20a923d4e9c0e91'; diff --git a/include/upgrader/sql/f4da0c9b-00ff231f.patch.sql b/include/upgrader/sql/f4da0c9b-00ff231f.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..2a28c82636ee7f3cd60cfb7e30cdff8c27edf645 --- /dev/null +++ b/include/upgrader/sql/f4da0c9b-00ff231f.patch.sql @@ -0,0 +1,17 @@ +/* + * @version=1.7RC2+ + * + * Add auto-reply template. + */ + +ALTER TABLE `%TABLE_PREFIX%email_template` + ADD `ticket_autoreply_subj` VARCHAR( 255 ) NOT NULL AFTER `ticket_autoresp_body` , + ADD `ticket_autoreply_body` TEXT NOT NULL AFTER `ticket_autoreply_subj`; + +UPDATE `%TABLE_PREFIX%email_template` + SET `ticket_autoreply_subj`='Support Ticket Opened [#%{ticket.number}]', + `ticket_autoreply_body`='%{ticket.name},\r\n\r\nA request for support has been created and assigned ticket #%{ticket.number} with the following auto-reply:\r\n\r\n%{response}\r\n\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not open another ticket. If need be, representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %{ticket.client_link}.'; + + -- update schema signature +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='00ff231f2ade8797a0e7f2a7fccd52f4'; diff --git a/include/upgrader/sql/f8856d56-abe9c0cb.patch.sql b/include/upgrader/sql/f8856d56-abe9c0cb.patch.sql index c0e5dd298482de89fda33bb04edf011d478f0b4d..e78ab80cc525fac35fc81061bc970ef89dc950f0 100644 --- a/include/upgrader/sql/f8856d56-abe9c0cb.patch.sql +++ b/include/upgrader/sql/f8856d56-abe9c0cb.patch.sql @@ -92,7 +92,7 @@ INSERT INTO `%TABLE_PREFIX%ticket_email_info` AND `thread_type` = 'M' ), `messageId`, `headers` FROM `%TABLE_PREFIX%ticket_message` - WHERE `messageId` IS NOT NULL; + WHERE `messageId` IS NOT NULL AND `messageId` <>''; -- Update attachment table UPDATE `%TABLE_PREFIX%ticket_attachment` diff --git a/l.php b/l.php index 657952e266f1498846db84c663e135997ecb242f..5e605c73cb3d48c32409c8ff89f727199bcbbba3 100644 --- a/l.php +++ b/l.php @@ -14,10 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require 'secure.inc.php'; - -global $_GET; -$url = $_GET['url']; -if (!$url) exit(); +$url = trim($_GET['url']); +if (!$url || !Validator::is_url($url)) exit('Invalid url'); ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> diff --git a/login.php b/login.php index f35693fd4083401a1aea72125e8cb1cc116710c7..834f00ddf72410de113271462f272ef38a4b9737 100644 --- a/login.php +++ b/login.php @@ -21,8 +21,17 @@ define('OSTCLIENTINC',TRUE); //make includes happy require_once(INCLUDE_DIR.'class.client.php'); require_once(INCLUDE_DIR.'class.ticket.php'); -if ($_POST) ClientSession::tryLogin($_POST['lticket'], $_POST['lemail']); -else ClientSession::tryLogin($_GET['t'], $_GET['e'], $_GET['a']); +if($_POST) { + + if(($user=Client::login(trim($_POST['lticket']), trim($_POST['lemail']), null, $errors))) { + //XXX: Ticket owner is assumed. + @header('Location: tickets.php?id='.$user->getTicketID()); + require_once('tickets.php'); //Just in case of 'header already sent' error. + exit; + } elseif(!$errors['err']) { + $errors['err'] = 'Authentication error - try again!'; + } +} $nav = new UserNav(); $nav->setActiveNav('status'); diff --git a/main.inc.php b/main.inc.php index abcbfeabdaee005bdd83abb7719ac163e0e531af..4a775b233bb00f45331b891e5f6fe1d4f202268d 100644 --- a/main.inc.php +++ b/main.inc.php @@ -62,9 +62,8 @@ /*############## 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-RC2'); //Shown on admin panel - define('SCHEMA_SIGNATURE','d0e37dca324648f1ce2d10528a6026d4'); //MD5 signature of the db schema. (used to trigger upgrades) - + define('THIS_VERSION','1.7-RC3'); //Shown on admin panel + define('SCHEMA_SIGNATURE','00ff231f2ade8797a0e7f2a7fccd52f4'); //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 @@ -113,7 +112,8 @@ require(INCLUDE_DIR.'mysql.php'); #CURRENT EXECUTING SCRIPT. - define('THISPAGE',Misc::currentURL()); + define('THISPAGE', Misc::currentURL()); + define('THISURI', $_SERVER['REQUEST_URI']); # This is to support old installations. with no secret salt. if(!defined('SECRET_SALT')) define('SECRET_SALT',md5(TABLE_PREFIX.ADMIN_EMAIL)); @@ -132,6 +132,7 @@ define('SYSLOG_TABLE',TABLE_PREFIX.'syslog'); define('SESSION_TABLE',TABLE_PREFIX.'session'); define('FILE_TABLE',TABLE_PREFIX.'file'); + define('FILE_CHUNK_TABLE',TABLE_PREFIX.'file_chunk'); define('STAFF_TABLE',TABLE_PREFIX.'staff'); define('DEPT_TABLE',TABLE_PREFIX.'department'); @@ -159,8 +160,10 @@ 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('FILTER_TABLE',TABLE_PREFIX.'filter'); + define('FILTER_RULE_TABLE',TABLE_PREFIX.'filter_rule'); + define('BANLIST_TABLE',TABLE_PREFIX.'email_banlist'); //Not in use anymore....as of v 1.7 define('SLA_TABLE',TABLE_PREFIX.'sla'); diff --git a/scp/admin.inc.php b/scp/admin.inc.php index 28db6fab7ba04c5100a26e6dadc615deb121b7be..8a41c54db7f690b3caec9ee7728345bba1570000 100644 --- a/scp/admin.inc.php +++ b/scp/admin.inc.php @@ -59,4 +59,7 @@ if($ost->isUpgradePending()) { //Admin navigation - overwrites what was set in staff.inc.php $nav = new AdminNav($thisstaff); + +//Page title. +$ost->setPageTitle('osTicket :: Admin Control Panel'); ?> diff --git a/scp/apikeys.php b/scp/apikeys.php index e393a31c5f2bea0647a238240ba5e9021ad3c9ab..89f649c1ce0e8f27c35b32a8c34565b4e929a662 100644 --- a/scp/apikeys.php +++ b/scp/apikeys.php @@ -41,53 +41,54 @@ if($_POST){ 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{ + $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(',', db_input($_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(',', db_input($_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'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.API_KEY_TABLE.' SET isactive=1 ' + .' WHERE id IN ('.implode(',', db_input($_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.'; + } + break; + case 'disable': + $sql='UPDATE '.API_KEY_TABLE.' SET isactive=0 ' + .' WHERE id IN ('.implode(',', db_input($_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'; + } + break; + case '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'; + break; + default: + $errors['err']='Unknown action - get technical help'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown action/command'; break; } } @@ -96,7 +97,7 @@ $page='apikeys.inc.php'; if($api || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='apikey.inc.php'; -$nav->setTabActive('settings'); +$nav->setTabActive('manage'); require(STAFFINC_DIR.'header.inc.php'); require(STAFFINC_DIR.$page); include(STAFFINC_DIR.'footer.inc.php'); diff --git a/scp/autocron.php b/scp/autocron.php index 08b65ba3481e65f99e6bb06567c3be46efb60631..4a3b00678539f628908743800177efc4097e54e1 100644 --- a/scp/autocron.php +++ b/scp/autocron.php @@ -30,13 +30,17 @@ 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']; +$caller = $thisstaff->getUserName(); + if($sec>180): //user can call cron once every 3 minutes. -require_once(INCLUDE_DIR.'class.cron.php'); +require_once(INCLUDE_DIR.'class.cron.php'); + +$thisstaff = null; //Clear staff obj to avoid false credit internal notes & auto-assignment Cron::TicketMonitor(); //Age tickets: We're going to age tickets regardless of cron settings. if($cfg && $cfg->isAutoCronEnabled()) { //ONLY fetch tickets if autocron is enabled! Cron::MailFetcher(); //Fetch mail. - $ost->logDebug('Auto Cron', 'Mail fetcher cron call ['.$thisstaff->getUserName().']'); -} + $ost->logDebug('Auto Cron', 'Mail fetcher cron call ['.$caller.']'); +} $_SESSION['lastcroncall']=time(); endif; diff --git a/scp/banlist.php b/scp/banlist.php index 081fde9b4ed0cd18713cab78e795c2e0eaf1df1b..d47552fa4d73e3b22892d9951f8015800ad73893 100644 --- a/scp/banlist.php +++ b/scp/banlist.php @@ -18,13 +18,13 @@ include_once(INCLUDE_DIR.'class.banlist.php'); /* Get the system ban list filter */ if(!($filter=Banlist::getFilter())) - $warn='System ban list is empty.'; + $warn = 'System ban list is empty.'; elseif(!$filter->isActive()) - $warn='SYSTEM BAN LIST filter is <b>DISABLED</b> - <a href="filters.php">enable here</a>.'; + $warn = 'SYSTEM BAN LIST filter is <b>DISABLED</b> - <a href="filters.php">enable here</a>.'; $rule=null; //ban rule obj. if($filter && $_REQUEST['id'] && !($rule=$filter->getRule($_REQUEST['id']))) - $errors['err']='Unknown or invalid ban list ID #'; + $errors['err'] = 'Unknown or invalid ban list ID #'; if($_POST && !$errors && $filter){ switch(strtolower($_POST['do'])){ @@ -64,50 +64,52 @@ if($_POST && !$errors && $filter){ 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{ + $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(',', db_input($_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(',', db_input($_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'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.FILTER_RULE_TABLE.' SET isactive=1 ' + .' WHERE filter_id='.db_input($filter->getId()) + .' AND id IN ('.implode(',', db_input($_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 ban status enabled"; + } else { + $errors['err'] = 'Unable to enable selected emails'; + } + break; + case 'disable': + $sql='UPDATE '.FILTER_RULE_TABLE.' SET isactive=0 ' + .' WHERE filter_id='.db_input($filter->getId()) + .' AND id IN ('.implode(',', db_input($_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'; + } + break; + case 'delete': + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($r=FilterRule::lookup($v)) && $r->getFilterId()==$filter->getId() && $r->delete()) + $i++; + } + if($i && $i==$count) + $msg = 'Selected emails deleted from banlist successfully'; + elseif($i>0) + $warn = "$i of $count selected emails deleted from banlist"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected emails'; - }else{ - $errors['err']='Unknown action'; + break; + default: + $errors['err'] = 'Unknown action - get technical help'; } } break; diff --git a/scp/canned.php b/scp/canned.php index 72a1680aa21b3777afc765a2f2256d2fd433782c..46d65601a49868728bf2ce838f69414365c6deb9 100644 --- a/scp/canned.php +++ b/scp/canned.php @@ -70,44 +70,48 @@ if($_POST && $thisstaff->canManageCannedResponses()) { $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(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())) { - if($num==$count) - $msg='Selected canned responses enabled'; - else - $warn="$num of $count selected canned responses enabled"; - } else { - $errors['err']='Unable to enable selected canned responses.'; - } - } elseif($_POST['disable']) { - $sql='UPDATE '.CANNED_TABLE.' SET isenabled=0 WHERE canned_id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())) { - if($num==$count) - $msg='Selected canned responses disabled'; - else - $warn="$num of $count selected canned responses disabled"; - } else { - $errors['err']='Unable to disable selected canned responses'; - } - }elseif($_POST['delete']) { - $i=0; - foreach($_POST['ids'] as $k=>$v) { - if(($c=Canned::lookup($v)) && $c->delete()) - $i++; - } + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.CANNED_TABLE.' SET isenabled=1 ' + .' WHERE canned_id IN ('.implode(',', db_input($_POST['ids'])).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected canned responses enabled'; + else + $warn = "$num of $count selected canned responses enabled"; + } else { + $errors['err'] = 'Unable to enable selected canned responses.'; + } + break; + case 'disable': + $sql='UPDATE '.CANNED_TABLE.' SET isenabled=0 ' + .' WHERE canned_id IN ('.implode(',', db_input($_POST['ids'])).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected canned responses disabled'; + else + $warn = "$num of $count selected canned responses disabled"; + } else { + $errors['err'] = 'Unable to disable selected canned responses'; + } + break; + case 'delete': + + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($c=Canned::lookup($v)) && $c->delete()) + $i++; + } - if($i==$count) - $msg='Selected canned responses deleted successfully'; - elseif($i>0) - $warn="$i of $count selected canned responses deleted"; - elseif(!$errors['err']) - $errors['err']='Unable to delete selected canned responses'; - - } else { - $errors['err']='Unknown command'; + if($i==$count) + $msg = 'Selected canned responses deleted successfully'; + elseif($i>0) + $warn="$i of $count selected canned responses deleted"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected canned responses'; + break; + default: + $errors['err']='Unknown command'; } } break; diff --git a/scp/categories.php b/scp/categories.php index 6b645fc847737e335feb0c474d868b819f8c7c2d..add40ed395c1b076660489ecb9569a099124684d 100644 --- a/scp/categories.php +++ b/scp/categories.php @@ -51,44 +51,49 @@ if($_POST){ $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(',', db_input($_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(',', db_input($_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'; + switch(strtolower($_POST['a'])) { + case 'make_public': + $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=1 ' + .' WHERE category_id IN ('.implode(',', db_input($_POST['ids'])).')'; - } else { - $errors['err']='Unknown command'; + 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.'; + } + break; + case 'make_private': + $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=0 ' + .' WHERE category_id IN ('.implode(',', db_input($_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'; + } + break; + case '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'; + break; + default: + $errors['err']='Unknown action/command'; } } break; diff --git a/scp/css/dropdown.css b/scp/css/dropdown.css new file mode 100644 index 0000000000000000000000000000000000000000..a95f6517b8b39a8f297fbf20c5234868a2543c40 --- /dev/null +++ b/scp/css/dropdown.css @@ -0,0 +1,136 @@ +/* + Based on jQuery dropdown + http://labs.abeautifulsite.net/jquery-dropdown/ +*/ + +.action-dropdown { + position: absolute; + z-index: 9999999; + display: none; + margin-top: 8px; +} +.action-dropdown ul { + text-align: left; + font-size: 13px; + min-width: 140px; + list-style: none; + background: #FFF; + border: solid 1px #DDD; + border: solid 1px rgba(0, 0, 0, 0.2); + border-radius: 6px; + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + overflow: visible; + padding: 4px 0; + margin: 0; +} +.action-dropdown ul li { + list-style: none; + padding: 0; + margin: 0; + line-height: 18px; +} +.action-dropdown ul li > a { + display: block; + color: #555; + text-decoration: none; + line-height: 18px; + padding: 3px 15px; + white-space: nowrap; +} +.action-dropdown ul li > a:hover { + background-color: #08C; + color: #FFF !important; + cursor: pointer; +} +.action-dropdown hr { + height: 1px; + border: none; + border-bottom: 1px solid #ddd; + margin: 5px 15px; + overflow: hidden; +} +.action-dropdown:before { + position: absolute; + top: -6px; + left: 9px; + content: ''; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #CCC; + border-bottom-color: rgba(0, 0, 0, 0.2); + display: inline-block; +} +.action-dropdown:after { + position: absolute; + top: -5px; + left: 10px; + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #FFF; + display: inline-block; +} + +.action-dropdown.anchor-right:before { + left: auto; + right: 9px; +} + +.action-dropdown.anchor-right:after { + left: auto; + right: 10px; +} + +.action-button { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + color: #777 !important; + display: inline-block; + border: 1px solid #aaa; + cursor: pointer; + font-size: 11px; + height: 18px; + overflow: hidden; + background-color: #dddddd; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #efefef), color-stop(100% #dddddd)); + background-image: -webkit-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -moz-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -ms-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -o-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: linear-gradient(top, #efefef 0%, #dddddd 100%); + padding: 0 5px; + text-decoration: none; + line-height:18px; + float:right; + margin-left:5px; +} +.action-button span, +.action-button a { + color: #777 !important; + display: inline-block; + float: left; +} +.action-button i.icon-caret-down { + background-color: #dddddd; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #efefef), color-stop(100% #dddddd)); + background-image: -webkit-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -moz-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -ms-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: -o-linear-gradient(top, #efefef 0%, #dddddd 100%); + background-image: linear-gradient(top, #efefef 0%, #dddddd 100%); + float: right; + height: 18px; + line-height: 18px; + margin-right: 0; + margin-left: 5px; + padding-left: 5px; + border-left: 1px solid #aaa; +} +.action-button a { + color: #777; + text-decoration: none; +} diff --git a/scp/css/scp.css b/scp/css/scp.css index 0042ed4306a406337f3a3deec2811467c06a325d..59964d4e61561ba78bc704e3e6ddb2d1a73af964 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -247,6 +247,13 @@ 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.ticket-settings { background:url(../images/icons/ticket-settings.gif) } +a.email-settings { background:url(../images/icons/email-settings.gif) } +a.kb-settings { background:url(../images/icons/kb-settings.gif) } +a.alert-settings { background:url(../images/icons/alert-settings.gif) } +a.email-autoresponders { background:url(../images/icons/email-autoresponders.gif) } + + a.sla { background:url(../images/icons/slas.png) } a.newsla { background:url(../images/icons/new_sla.png) } @@ -259,7 +266,8 @@ 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.ticketFilters { background:url(../images/icons/ticket_filters.png) } +a.newTicketFilter { background:url(../images/icons/new_ticket_filter.png) } a.emailSettings { background:url(../images/icons/emails.png) } a.emailDiagnostic { background:url(../images/icons/email_diagnostic.gif) } @@ -403,6 +411,11 @@ table.list tbody td { background: #fff; padding: 1px; padding-left:2px; vertical 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; } +/* row highlighting on hover + select */ +table.list tbody tr:hover td, table.list tbody tr.highlight td { background: #FFFFDD; } +/* disabled highlighting on nohover */ +table.list tbody tr:hover td.nohover, table.list tbody tr.highlight td.nohover {} + table.list tfoot td { background:#eee; @@ -605,6 +618,13 @@ h2 { color:#0A568E; } +h2 i { + font-size:12px; + top:-2px; + position:relative; + color:#0a0; +} + h2 span { color:#000; } h3 { @@ -614,6 +634,11 @@ h3 { color:#444; } +.has_bottom_border { + padding-bottom:5px; + border-bottom:1px solid #ddd; +} + .ticket_info th { text-align:left; } @@ -1145,38 +1170,31 @@ time { margin-right:20px; } -/* Advanced Search & Ticket print options */ - -#search_overlay { - background:#000; - position:absolute; - display:none; - z-index:1000; -} - -#advanced-search, #advanced-search *, #print-options, #print-options * { +/* dialog */ +.dialog, .dialog * { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; } -#advanced-search, #print-options { +.dialog { position:absolute; padding:1em; - width:640px; - height:360px; + width:500px; + height:250px; + height:auto !important; background:#fff; border:1px solid #2a67ac; display:none; z-index:1200; } -#print-options { - width:500px; - height:250px; +.dialog#advanced-search { + width:640px !important; + height:360px; } -#print-options hr { +.dialog hr { height: 1px; border: 0; background: #aaa; @@ -1188,7 +1206,7 @@ time { filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00aaaaaa', endColorstr='#00aaaaaa',GradientType=1 ); /* IE6-9 */ } -#advanced-search h3, #print-options h3 { +.dialog h3 { color:#2a67ac; font-size:20px; margin:0; @@ -1196,14 +1214,14 @@ time { display:inline-block; } -#advanced-search a.close, #print-options a.close { +.dialog a.close { display:inline-block; float:right; font-size:16px; color:#777; } -#advanced-search form, #print-options form { +.dialog form { clear:both; padding-top:2em; width:100%; @@ -1213,31 +1231,31 @@ time { display:none; } -#advanced-search fieldset, #print-options fieldset { +.dialog fieldset { margin:0; padding:0 0; border:none; overflow:hidden; } -#advanced-search label, #print-options label { +.dialog label { width:100px; display:inline-block; text-align:right; padding:10px; } -#advanced-search fieldset input, #print-options fieldset input { +.dialog fieldset input { border:1px solid #ccc; background:#fff; } -#advanced-search fieldset select, #print-options fieldset select { +.dialog fieldset select { width:170px; display:inline-block; } -#advanced-search fieldset span, #print-options fieldset span { +.dialog fieldset span { width:50px; display:inline-block; text-align:center; @@ -1271,13 +1289,9 @@ time { text-align:center; } -#advanced-search input[type="submit"], -#advanced-search input[type="reset"], -#advanced-search input[type="button"], -#print-options input[type="submit"], -#print-options input[type="reset"], -#print-options input[type="button"] -{ +.dialog input[type="submit"], +.dialog input[type="reset"], +.dialog input[type="button"] { display:inline-block; margin:0; height:24px; @@ -1289,15 +1303,13 @@ time { color: #333; } -#advanced-search input[type="reset"], -#advanced-search input[type="button"], -#print-options input[type="reset"], -#print-options input[type="button"] { +.dialog input[type="reset"], +.dialog input[type="button"] { opacity:0.7; } -#advanced-search input[type=submit]:hover, #advanced-search input[type=submit]:active, -#advanced-search input[type=reset]:hover, #advanced-search input[type=reset]:active { +.dialog input[type=submit]:hover, .dialog input[type=submit]:active, +.dialog input[type=reset]:hover, .dialog input[type=reset]:active { background-position:bottom left; } diff --git a/scp/dashboard.php b/scp/dashboard.php index 3b41ccfdfc1a2f4e123c501d8b0a1088584cffb3..2f4f1c42ffbe8e73f8f3da939fa1da22faec6e64 100644 --- a/scp/dashboard.php +++ b/scp/dashboard.php @@ -57,8 +57,7 @@ require(STAFFINC_DIR.'header.inc.php'); <hr/> <h2>Statistics</h2> -<p>Statistics of tickets organized by department, help topic, and staff -member.</p> +<p>Statistics of tickets organized by department, help topic, and staff.</p> <ul class="nav nav-tabs" id="tabular-navigation"></ul> <div id="table-here"></div> diff --git a/scp/departments.php b/scp/departments.php index ef1acb12d62adcaa3c6285a210e7aa9d3f57fc29..ff92b38fcb42487366d0246aafbdceb11be96b17 100644 --- a/scp/departments.php +++ b/scp/departments.php @@ -39,63 +39,65 @@ if($_POST){ 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.'; + $errors['err'] = 'You must select at least one department'; + }elseif(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(',', db_input($_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(',', db_input($_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(',', db_input($_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++; + switch(strtolower($_POST['a'])) { + case 'make_public': + $sql='UPDATE '.DEPT_TABLE.' SET ispublic=1 ' + .' WHERE dept_id IN ('.implode(',', db_input($_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.'; + } + break; + case 'make_private': + $sql='UPDATE '.DEPT_TABLE.' SET ispublic=0 ' + .' WHERE dept_id IN ('.implode(',', db_input($_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!'; + } + break; + case '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(',', db_input($_POST['ids'])).')'; + list($members)=db_fetch_row(db_query($sql)); + if($members) + $errors['err']='Departments with staff 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.'; } - 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 - get technical help'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown action/command'; break; } } @@ -104,7 +106,7 @@ $page='departments.inc.php'; if($dept || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='department.inc.php'; -$nav->setTabActive('depts'); +$nav->setTabActive('staff'); 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 index b0d32bb59bf9d415275aaada58c96cf3df892629..bdb1f5f5cb7132c7f2b590b2c6eba607203a5669 100644 --- a/scp/emails.php +++ b/scp/emails.php @@ -41,19 +41,18 @@ if($_POST){ 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{ + $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(',', db_input($_POST['ids'])). - ') OR autoresp_email_id IN ('. - implode(',', db_input($_POST['ids'])).')'; + $sql='SELECT count(dept_id) FROM '.DEPT_TABLE.' dept ' + .' WHERE email_id IN ('.implode(',', db_input($_POST['ids'])).') ' + .' OR autoresp_email_id IN ('.implode(',', db_input($_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']){ + if($depts>0) { + $errors['err'] = 'One or more of the selected emails is being used by a department. Remove association first!'; + } elseif(!strcasecmp($_POST['a'], 'delete')) { $i=0; foreach($_POST['ids'] as $k=>$v) { if($v!=$cfg->getDefaultEmailId() && ($e=Email::lookup($v)) && $e->delete()) @@ -61,19 +60,19 @@ if($_POST){ } if($i && $i==$count) - $msg='Selected emails deleted successfully'; + $msg = 'Selected emails deleted successfully'; elseif($i>0) - $warn="$i of $count selected emails deleted"; + $warn = "$i of $count selected emails deleted"; elseif(!$errors['err']) - $errors['err']='Unable to delete selected emails'; + $errors['err'] = 'Unable to delete selected emails'; - }else { - $errors['err']='Unknown command'; + } else { + $errors['err'] = 'Unknown action - get technical help'; } } break; default: - $errors['err']='Unknown action'; + $errors['err'] = 'Unknown action/command'; break; } } diff --git a/scp/filters.php b/scp/filters.php index 4ce0f30765f5192617f22345c00571f26d206af2..dc8443f74f207fea222279bcb5c93d74e444be6b 100644 --- a/scp/filters.php +++ b/scp/filters.php @@ -45,53 +45,55 @@ if($_POST){ 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{ + $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(',', db_input($_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(',', db_input($_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'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.FILTER_TABLE.' SET isactive=1 ' + .' WHERE id IN ('.implode(',', db_input($_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'; + } + break; + case 'disable': + $sql='UPDATE '.FILTER_TABLE.' SET isactive=0 ' + .' WHERE id IN ('.implode(',', db_input($_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'; + } + break; + case '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'; + break; + default: + $errors['err']='Unknown action - get technical help'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown commande/action'; break; } } @@ -100,7 +102,7 @@ $page='filters.inc.php'; if($filter || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='filter.inc.php'; -$nav->setTabActive('emails'); +$nav->setTabActive('manage'); 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 index 22b1bae0acb36959fff05ec3f75323a691053920..849acecfdff57b037c75851205e0cab6050a2ef4 100644 --- a/scp/groups.php +++ b/scp/groups.php @@ -39,45 +39,52 @@ if($_POST){ break; case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { - $errors['err']='You must select at least one group.'; - }else{ + $errors['err'] = 'You must select at least one group.'; + } elseif(in_array($thisstaff->getGroupId(), $_POST['ids'])) { + $errors['err'] = "As an admin, you can't disable/delete a group you belong to - you might lockout all admins!"; + } else { $count=count($_POST['ids']); - if($_POST['enable']){ - $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=1, updated=NOW() WHERE group_id IN ('. - implode(',', db_input($_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(',', db_input($_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(($g=Group::lookup($v)) && $g->delete()) - $i++; - } + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=1, updated=NOW() ' + .' WHERE group_id IN ('.implode(',', db_input($_POST['ids'])).')'; - 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!'; + 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'; + } + break; + case 'disable': + $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=0, updated=NOW() ' + .' WHERE group_id IN ('.implode(',', db_input($_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'; + } + break; + case 'delete': + foreach($_POST['ids'] as $k=>$v) { + if(($g=Group::lookup($v)) && $g->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'; + break; + default: + $errors['err'] = 'Unknown action. Get technical help!'; } } break; diff --git a/scp/helptopics.php b/scp/helptopics.php index 5bd1ded7048f26307f2cd18d09d6b3a946f13f65..6bb2f7862afacefc00e3e45921c34c4c5a481c2a 100644 --- a/scp/helptopics.php +++ b/scp/helptopics.php @@ -41,53 +41,58 @@ if($_POST){ 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{ + $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(',', db_input($_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(',', db_input($_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'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.TOPIC_TABLE.' SET isactive=1 ' + .' WHERE topic_id IN ('.implode(',', db_input($_POST['ids'])).')'; - }else { - $errors['err']='Unknown action'; + 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.'; + } + break; + case 'disable': + $sql='UPDATE '.TOPIC_TABLE.' SET isactive=0 ' + .' WHERE topic_id IN ('.implode(',', db_input($_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)'; + } + break; + case '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'; + + break; + default: + $errors['err']='Unknown action - get technical help.'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown command/action'; break; } } @@ -96,7 +101,7 @@ $page='helptopics.inc.php'; if($topic || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='helptopic.inc.php'; -$nav->setTabActive('topics'); +$nav->setTabActive('manage'); require(STAFFINC_DIR.'header.inc.php'); require(STAFFINC_DIR.$page); include(STAFFINC_DIR.'footer.inc.php'); diff --git a/scp/images/icons/alert-settings.gif b/scp/images/icons/alert-settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c8a0b1debd05f87bb53b0d43fb10f153738df1d Binary files /dev/null and b/scp/images/icons/alert-settings.gif differ diff --git a/scp/images/icons/email-autoresponders.gif b/scp/images/icons/email-autoresponders.gif new file mode 100644 index 0000000000000000000000000000000000000000..5c118b46b3daf67261a92bd252a97ae4a34b4153 Binary files /dev/null and b/scp/images/icons/email-autoresponders.gif differ diff --git a/scp/images/icons/email-settings.gif b/scp/images/icons/email-settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..6dc8aa0023e6d69355200e53ac492d61346f83d3 Binary files /dev/null and b/scp/images/icons/email-settings.gif differ diff --git a/scp/images/icons/kb-settings.gif b/scp/images/icons/kb-settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..991f3c812f0216eb0957d619a92da2e55d22131c Binary files /dev/null and b/scp/images/icons/kb-settings.gif differ diff --git a/scp/images/icons/new_ticket_filter.png b/scp/images/icons/new_ticket_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..00c091ebff2b5c2f20729f4f0b16a6405022b5ea Binary files /dev/null and b/scp/images/icons/new_ticket_filter.png differ diff --git a/scp/images/icons/ticket-settings.gif b/scp/images/icons/ticket-settings.gif new file mode 100644 index 0000000000000000000000000000000000000000..f5d5234a95f4b5d26e7a8eed41054a9377077d90 Binary files /dev/null and b/scp/images/icons/ticket-settings.gif differ diff --git a/scp/images/icons/ticket_filters.png b/scp/images/icons/ticket_filters.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2065212f29d70d1f17f316de5f6b8a77b3f950 Binary files /dev/null and b/scp/images/icons/ticket_filters.png differ diff --git a/scp/images/icons/ticket_source_other.png b/scp/images/icons/ticket_source_other.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a78f27412669beb75c2371d035d5aa104bdce4 Binary files /dev/null and b/scp/images/icons/ticket_source_other.png differ diff --git a/scp/js/dashboard.inc.js b/scp/js/dashboard.inc.js index cc84661cf2ce393934fa5b0f43a28c222bbe9935..0f9e31a54dccdff96388d7304b0187f2d8dc9fad 100644 --- a/scp/js/dashboard.inc.js +++ b/scp/js/dashboard.inc.js @@ -115,7 +115,7 @@ var first=true; for (key in json) { $('#tabular-navigation') - .append($('<li>').attr((first) ? {class:"active"} : {}) + .append($('<li>').attr(first ? {'class':'active'} : {}) .append($('<a>') .click(build_table) .attr({'table-group':key,'href':'#'}) diff --git a/scp/js/jquery.dropdown.js b/scp/js/jquery.dropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..c0604e75d2686c457c5ccb56c4c7be9122dbe970 --- /dev/null +++ b/scp/js/jquery.dropdown.js @@ -0,0 +1,74 @@ +/* + * jQuery dropdown: A simple dropdown plugin + * + * Inspired by Bootstrap: http://twitter.github.com/bootstrap/javascript.html#dropdowns + * + * Copyright 2011 Cory LaViska for A Beautiful Site, LLC. (http://abeautifulsite.net/) + * + * Dual licensed under the MIT or GPL Version 2 licenses + * +*/ +if(jQuery) (function($) { + + $.extend($.fn, { + dropdown: function(method, data) { + + switch( method ) { + case 'hide': + hideDropdowns(); + return $(this); + case 'attach': + return $(this).attr('data-dropdown', data); + case 'detach': + hideDropdowns(); + return $(this).removeAttr('data-dropdown'); + case 'disable': + return $(this).addClass('dropdown-disabled'); + case 'enable': + hideDropdowns(); + return $(this).removeClass('dropdown-disabled'); + } + + } + }); + + function showMenu(event) { + + var trigger = $(this), + dropdown = $( $(this).attr('data-dropdown') ), + isOpen = trigger.hasClass('dropdown-open'); + + event.preventDefault(); + event.stopPropagation(); + + hideDropdowns(); + + if( isOpen || trigger.hasClass('dropdown-disabled') ) return; + + dropdown.css({ + left: dropdown.hasClass('anchor-right') ? + trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left, + top: trigger.offset().top + trigger.outerHeight() + }).show(); + trigger.addClass('dropdown-open'); + } + + function hideDropdowns(event) { + + var targetGroup = event ? $(event.target).parents().andSelf() : null; + if( targetGroup && targetGroup.is('.action-dropdown') && !targetGroup.is('a') ) return; + + $('body') + .find('.action-dropdown').hide().end() + .find('[data-dropdown]').removeClass('dropdown-open'); + } + + $(function () { + $('body').on('click.dropdown', '[data-dropdown]', showMenu); + $('html').on('click.dropdown', hideDropdowns); + if( !$.browser.msie || ($.browser.msie && $.browser.version >= 9) ) { + $(window).on('resize.dropdown', hideDropdowns); + } + }); + +})(jQuery); diff --git a/scp/js/scp.js b/scp/js/scp.js index 8730685c0e2134d18d6a29e9f9f74cf66daaffa8..71d0e69a823e0869f38ec3e9a5562bec296a7baa 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -6,47 +6,11 @@ */ -function selectAll(formObj,task,highlight){ - var highlight = highlight || false; - - for (var i=0;i < formObj.length;i++){ - var e = formObj.elements[i]; - if (e.type == 'checkbox' && !e.disabled){ - if(task==0){ - e.checked =false; - }else if(task==1){ - e.checked = true; - }else{ - e.checked = (e.checked) ? false : true; - } - - if(highlight && 0) { - highLight(e.value,e.checked); - } - } - } - - return false; -} - -function reset_all(formObj){ - return selectAll(formObj,0,true); -} -function select_all(formObj,highlight){ - return selectAll(formObj,1,highlight); -} -function toogle_all(formObj,highlight){ - - var highlight = highlight || false; - return selectAll(formObj,2,highlight); -} +function checkbox_checker(formObj, min, max) { - - -function checkbox_checker(formObj, min,max) { - - - var checked=$("input[type=checkbox]:checked").length; + var max = max || 0; + var min = min || 1; + var checked=$('input:checkbox:checked', formObj).length; var action= action?action:"process"; if (max>0 && checked > max ){ msg="You're limited to only " + max + " selections.\n" @@ -69,6 +33,75 @@ $(document).ready(function(){ $("input:not(.dp):visible:enabled:first").focus(); $('table.list tbody tr:odd').addClass('odd'); + $('table.list input:checkbox').bind('click, change', function() { + $(this) + .parents("tr:first") + .toggleClass("highlight", this.checked); + }); + + $('table.list input:checkbox:checked').trigger('change'); + + $('#selectAll').click(function(e) { + e.preventDefault(); + var target = $(this).attr('href').substr(1, $(this).attr('href').length); + $(this).closest('form') + .find('input:enabled:checkbox.'+target) + .prop('checked', true) + .trigger('change'); + + return false; + }); + + + $('#selectNone').click(function(e) { + e.preventDefault(); + var target = $(this).attr('href').substr(1, $(this).attr('href').length); + $(this).closest('form') + .find('input:enabled:checkbox.'+target) + .prop('checked', false) + .trigger('change'); + return false; + }); + + $('#selectToggle').click(function(e) { + e.preventDefault(); + var target = $(this).attr('href').substr(1, $(this).attr('href').length); + $(this).closest('form') + .find('input:enabled:checkbox.'+target) + .each(function() { + $(this) + .prop('checked', !$(this).is(':checked')) + .trigger('change'); + }); + return false; + }); + + $('#actions input:submit.button').bind('click', function(e) { + + var formObj = $(this).closest('form'); + e.preventDefault(); + if($('.dialog#confirm-action p#'+this.name+'-confirm').length == 0) { + alert('Unknown action '+this.name+' - get technical help.'); + } else if(checkbox_checker(formObj, 1)) { + var action = this.name; + $('.dialog#confirm-action').undelegate('.confirm'); + $('.dialog#confirm-action').delegate('input.confirm', 'click.confirm', function(e) { + e.preventDefault(); + $('.dialog#confirm-action').hide(); + $('#overlay').hide(); + $('input#action', formObj).val(action); + formObj.submit(); + return false; + }); + $('#overlay').show(); + $('.dialog#confirm-action .confirm-action').hide(); + $('.dialog#confirm-action p#'+this.name+'-confirm') + .show() + .parent('div').show().trigger('click'); + } + + return false; + }); if($.browser.msie) { $('.inactive').mouseenter(function() { @@ -110,7 +143,7 @@ $(document).ready(function(){ return true; }); - $('select#setting_options').change(function() { + $('select#tpl_options').change(function() { $(this).closest('form').submit(); }); @@ -279,16 +312,31 @@ $(document).ready(function(){ property: "email" }); - /* advanced search */ - $("#overlay, #search_overlay").css({ + //Overlay + $('#overlay').css({ opacity : 0.3, top : 0, left : 0, width : $(window).width(), height : $(window).height() }); + + //Dialog + $('.dialog').css({ + top : ($(window).height() /5), + left : ($(window).width() / 2 - 300) + }); + + $('.dialog').delegate('input.close, a.close', 'click', function(e) { + e.preventDefault(); + $(this).parents('div.dialog').hide() + $('#overlay').hide(); + + return false; + }); - $("#advanced-search").css({ + /* advanced search */ + $('.dialog#advanced-search').css({ top : ($(window).height() / 6), left : ($(window).width() / 2 - 300) }); @@ -296,15 +344,11 @@ $(document).ready(function(){ $('#go-advanced').click(function(e) { e.preventDefault(); $('#result-count').html(''); - $('#search_overlay').show(); + $('#overlay').show(); $('#advanced-search').show(); }); - $('#advanced-search').delegate('a.close, input.close', 'click', function(e) { - e.preventDefault(); - $('#advanced-search').hide() - $('#search_overlay').hide(); - }).delegate('#status', 'change', function() { + $('#advanced-search').delegate('#status', 'change', function() { switch($(this).val()) { case 'closed': $('select#assignee').find('option:first').attr('selected', 'selected').parent('select'); diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 2913c225cf0579f3587442831a135bb3464b53f8..65ee84aad0882649f8aec44daf447b45d38e64d9 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -93,13 +93,18 @@ var autoLock = { Init: function(config) { - //make sure we are on ticket view page! - void(autoLock.form=document.forms['reply']); - if(!autoLock.form || !autoLock.form.id.value) { - return; + //make sure we are on ticket view page & locking is enabled! + var fObj=$('form#reply'); + if(!fObj + || !$(':input[name=id]',fObj).length + || !$(':input[name=locktime]',fObj).length + || $(':input[name=locktime]',fObj).val()==0) { + return; } - void(autoLock.tid=parseInt(autoLock.form.id.value)); + void(autoLock.tid=parseInt($(':input[name=id]',fObj).val())); + void(autoLock.lockTime=parseInt($(':input[name=locktime]',fObj).val())); + autoLock.lockId=0; autoLock.timerId=0; autoLock.lasteventTime=0; @@ -108,9 +113,6 @@ var autoLock = { autoLock.renewTime=0; autoLock.renewFreq=0; //renewal frequency in seconds...based on returned lock time. autoLock.time=0; - if(config && config.ticket_lock_time) - autoLock.timeTime=config.ticket_lock_time - autoLock.lockAttempts=0; //Consecutive lock attempt errors autoLock.maxattempts=2; //Maximum failed lock attempts before giving up. autoLock.warn=true; @@ -316,26 +318,44 @@ jQuery(function($) { } } }); - - //Ticket print options - $("#print-options").css({ - top : ($(window).height() /5), - left : ($(window).width() / 2 - 300) - }); + //Start watching the form for activity. + autoLock.Init(); + + /*** Ticket Actions **/ + //print options $('a#ticket-print').click(function(e) { e.preventDefault(); $('#overlay').show(); - $('#print-options').show(); + $('.dialog#print-options').show(); return false; }); - $('#print-options').delegate('a.close, input.close', 'click', function(e) { + //ticket status (close & reopen) + $('a#ticket-close, a#ticket-reopen').click(function(e) { e.preventDefault(); - $('#print-options').hide() - $('#overlay').hide(); + $('#overlay').show(); + $('.dialog#ticket-status').show(); + return false; + }); + + //ticket actions confirmation - Delete + more + $('a#ticket-delete, a#ticket-claim, #action-dropdown-more li a').click(function(e) { + e.preventDefault(); + if($('.dialog#confirm-action '+$(this).attr('href')+'-confirm').length) { + var action = $(this).attr('href').substr(1, $(this).attr('href').length); + $('.dialog#confirm-action #action').val(action); + $('#overlay').show(); + $('.dialog#confirm-action .confirm-action').hide(); + $('.dialog#confirm-action p'+$(this).attr('href')+'-confirm') + .show() + .parent('div').show().trigger('click'); + + } else { + alert('Unknown action '+$(this).attr('href')+'- get technical help.'); + } + + return false; }); - //Start watching the form for activity. - autoLock.Init(); }); diff --git a/scp/js/ticket.min.js b/scp/js/ticket.min.js new file mode 100644 index 0000000000000000000000000000000000000000..cd7c0b6ac3b0491b5437e036db2d924c9e58bb18 --- /dev/null +++ b/scp/js/ticket.min.js @@ -0,0 +1,15 @@ +/********************************************************************* + ticket.js + + Ticket utility loaded on ticket view! + + Useful for UI config settings, ticket locking ...etc + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2012 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/var autoLock={addEvent:function(e,t,n,r){if(e.addEventListener){e.addEventListener(t,n,r);return!0}if(e.attachEvent)return e.attachEvent("on"+t,n);e["on"+t]=n},removeEvent:function(e,t,n,r){if(e.removeEventListener){e.removeEventListener(t,n,r);return!0}if(e.detachEvent)return e.detachEvent("on"+t,n);e["on"+t]=null},handleEvent:function(e){if(!autoLock.lockId){var t=(new Date).getTime();if(autoLock.retry&&(!autoLock.lastattemptTime||t-autoLock.lastattemptTime>5e3)){autoLock.acquireLock(e,autoLock.warn);autoLock.lastattemptTime=(new Date).getTime()}}else autoLock.renewLock(e);autoLock.lasteventTime||autoLock.addEvent(window,"beforeunload",autoLock.discardWarning,!0);autoLock.lasteventTime=(new Date).getTime()},watchForm:function(e,t){if(!e||!e.length)return;autoLock.addEvent(e,"submit",autoLock.onSubmit,!0);for(var n=0;n<e.length;n++)switch(e[n].type){case"textarea":case"text":autoLock.addEvent(e[n],"keyup",autoLock.handleEvent,!0);break;case"select-one":case"select-multiple":e.name!="reply"&&autoLock.addEvent(e[n],"change",autoLock.handleEvent,!0);break;default:}},watchDocument:function(){for(var e=0;e<document.forms.length;e++){if(!document.forms[e].id.value||parseInt(document.forms[e].id.value)!=autoLock.tid)continue;autoLock.watchForm(document.forms[e],autoLock.checkLock)}},Init:function(e){void (autoLock.form=document.forms.reply);if(!autoLock.form||!autoLock.form.id.value)return;void (autoLock.tid=parseInt(autoLock.form.id.value));autoLock.lockId=0;autoLock.timerId=0;autoLock.lasteventTime=0;autoLock.lastattemptTime=0;autoLock.acquireTime=0;autoLock.renewTime=0;autoLock.renewFreq=0;autoLock.time=0;e&&e.ticket_lock_time&&(autoLock.timeTime=e.ticket_lock_time);autoLock.lockAttempts=0;autoLock.maxattempts=2;autoLock.warn=!0;autoLock.retry=!0;autoLock.watchDocument();autoLock.resetTimer();autoLock.addEvent(window,"unload",autoLock.releaseLock,!0)},onSubmit:function(e){if(e.type=="submit"){autoLock.removeEvent(window,"beforeunload",autoLock.discardWarning,!0);if(autoLock.warn&&!autoLock.lockId&&autoLock.lasteventTime){var t=confirm("Unable to acquire a lock on the ticket. Someone else could be working on the same ticket. Please confirm if you wish to continue anyways.");if(!t){e.returnValue=!1;e.cancelBubble=!0;e.preventDefault&&e.preventDefault();return!1}}}return!0},acquireLock:function(e,t){if(!autoLock.tid)return!1;var t=t||!1;autoLock.lockId?autoLock.renewLock(e):$.ajax({type:"POST",url:"ajax.php/tickets/"+autoLock.tid+"/lock",dataType:"json",cache:!1,success:function(e){autoLock.setLock(e,"acquire",t)}}).done(function(){}).fail(function(){});return autoLock.lockId},renewLock:function(e){if(!autoLock.lockId)return!1;var t=(new Date).getTime();(!autoLock.lastcheckTime||t-autoLock.lastcheckTime>=autoLock.renewFreq*1e3)&&$.ajax({type:"POST",url:"ajax.php/tickets/"+autoLock.tid+"/lock/"+autoLock.lockId+"/renew",dataType:"json",cache:!1,success:function(e){autoLock.setLock(e,"renew",autoLock.warn)}}).done(function(){}).fail(function(){})},releaseLock:function(e){if(!autoLock.tid)return!1;$.ajax({type:"POST",url:"ajax.php/tickets/"+autoLock.tid+"/lock/"+autoLock.lockId+"/release",data:"delete",cache:!1,success:function(){}}).done(function(){}).fail(function(){})},setLock:function(t,n,r){var r=r||!1;if(!t)return!1;if(t.id){autoLock.renewFreq=t.time?t.time/2:30;autoLock.lastcheckTime=(new Date).getTime()}autoLock.lockId=t.id;switch(n){case"renew":if(!t.id&&t.retry){autoLock.lockAttempts=1;autoLock.acquireLock(e,!1)}break;case"acquire":if(!t.id){autoLock.lockAttempts++;if(r&&(!t.retry||autoLock.lockAttempts>=autoLock.maxattempts)){autoLock.retry=!1;alert("Unable to lock the ticket. Someone else could be working on the same ticket.")}}}},discardWarning:function(e){e.returnValue="Any changes or info you've entered will be discarded!"},monitorEvents:function(){},clearTimer:function(){clearTimeout(autoLock.timerId)},resetTimer:function(){clearTimeout(autoLock.timerId);autoLock.timerId=setTimeout(function(){autoLock.monitorEvents()},3e4)}};jQuery(function(e){e("#response_options form").hide();e("#ticket_notes").hide();if(location.hash!=""&&e("#response_options "+location.hash).length){e("#response_options "+location.hash+"_tab").addClass("active");e("#response_options "+location.hash).show()}else if(location.hash=="#notes"&&e("#ticket_notes").length){e("#response_options #note_tab").addClass("active");e("#response_options form").hide();e("#response_options #note").show();e("#ticket_thread").hide();e("#ticket_notes").show();e("#toggle_ticket_thread").removeClass("active");e("#toggle_notes").addClass("active")}else{e("#response_options ul li:first a").addClass("active");e("#response_options "+e("#response_options ul li:first a").attr("href")).show()}e("#reply_tab").click(function(){e(this).removeClass("tell")});e("#note_tab").click(function(){e("#response").val()!=""&&e("#reply_tab").addClass("tell")});e("#response_options ul li a").click(function(t){t.preventDefault();e("#response_options ul li a").removeClass("active");e(this).addClass("active");e("#response_options form").hide();e("#response_options "+e(this).attr("href")).show();e("#msg_error, #msg_notice, #msg_warning").fadeOut()});e("#toggle_ticket_thread, #toggle_notes, .show_notes").click(function(t){t.preventDefault();e("#threads a").removeClass("active");if(e(this).attr("id")=="toggle_ticket_thread"){e("#ticket_notes").hide();e("#ticket_thread").show();e("#toggle_ticket_thread").addClass("active");e("#reply_tab").removeClass("tell").click()}else{e("#ticket_thread").hide();e("#ticket_notes").show();e("#toggle_notes").addClass("active");e("#note_tab").click();e("#response").val()!=""&&e("#reply_tab").addClass("tell")}});e("#print-options").css({top:e(window).height()/5,left:e(window).width()/2-300});e("a#ticket-print").click(function(t){t.preventDefault();e("#overlay").show();e("#print-options").show();return!1});e("#print-options").delegate("a.close, input.close","click",function(t){t.preventDefault();e("#print-options").hide();e("#overlay").hide()});autoLock.Init()}); \ No newline at end of file diff --git a/scp/l.php b/scp/l.php index 2c66c2835eefcafb6711416228fa985878c43428..93fff3a24894612017f53ff0bb0a119b656f8b9d 100644 --- a/scp/l.php +++ b/scp/l.php @@ -14,10 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require_once 'staff.inc.php'; - -global $_GET; -$url = $_GET['url']; -if (!$url) exit(); +$url = trim($_GET['url']); +if (!$url || !Validator::is_url($url)) exit('Invalid url'); ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> diff --git a/scp/login.php b/scp/login.php index 6a28e0f45130f26f733f8eca82066565f5e2d9dc..5ba5e28e0568afb7626001cc00d76d35b02f2f93 100644 --- a/scp/login.php +++ b/scp/login.php @@ -19,20 +19,19 @@ if(!defined('INCLUDE_DIR')) die('Fatal Error. Kwaheri!'); require_once(INCLUDE_DIR.'class.staff.php'); require_once(INCLUDE_DIR.'class.csrf.php'); -$msg=$_SESSION['_staff']['auth']['msg']; -$msg=$msg?$msg:'Authentication Required'; -if($_POST && (!empty($_POST['username']) && !empty($_POST['passwd']))){ +$dest = $_SESSION['_staff']['auth']['dest']; +$msg = $_SESSION['_staff']['auth']['msg']; +$msg = $msg?$msg:'Authentication Required'; +if($_POST) { //$_SESSION['_staff']=array(); #Uncomment to disable login strikes. - $msg='Invalid login'; - if(($user=Staff::login($_POST['username'],$_POST['passwd'],$errors))){ - $dest=$_SESSION['_staff']['auth']['dest']; + if(($user=Staff::login($_POST['username'], $_POST['passwd'], $errors))){ $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php'; @header("Location: $dest"); require_once('index.php'); //Just incase header is messed up. exit; - }elseif(!$errors['err']){ - $errors['err']='Login error - try again'; } + + $msg = $errors['err']?$errors['err']:'Invalid login'; } define("OSTSCPINC",TRUE); //Make includes happy! include_once(INCLUDE_DIR.'staff/login.tpl.php'); diff --git a/scp/logs.php b/scp/logs.php index e29ef3702a50cd8949cc355b99b5a12dcb0ceadd..1e3eb57ba0b8548b40f41a401b723e475f4461b3 100644 --- a/scp/logs.php +++ b/scp/logs.php @@ -19,26 +19,27 @@ if($_POST){ switch(strtolower($_POST['do'])){ case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { - $errors['err']='You must select at least one log to delete'; - }else{ + $errors['err'] = 'You must select at least one log to delete'; + } else { $count=count($_POST['ids']); - if($_POST['delete']){ - $sql='DELETE FROM '.SYSLOG_TABLE.' WHERE log_id IN (' - .implode(',', db_input($_POST['ids'])).')'; + if($_POST['a'] && !strcasecmp($_POST['a'], 'delete')) { + + $sql='DELETE FROM '.SYSLOG_TABLE + .' WHERE log_id IN ('.implode(',', db_input($_POST['ids'])).')'; if(db_query($sql) && ($num=db_affected_rows())){ if($num==$count) $msg='Selected logs deleted successfully'; else $warn="$num of $count selected logs deleted"; - }elseif(!$errors['err']) + } elseif(!$errors['err']) $errors['err']='Unable to delete selected logs'; - }else{ - $errors['err']='Unknown command'; + } else { + $errors['err']='Unknown action - get technical help'; } } break; default: - $errors['err']='Unknown option'; + $errors['err']='Unknown command/action'; break; } } diff --git a/scp/settings.php b/scp/settings.php index 9058fe42ca52c4f9b428bbc52f2ed2cb61f80a8a..0a3f97a912d8fdf6d92dc42ddf035f36bc61b12f 100644 --- a/scp/settings.php +++ b/scp/settings.php @@ -15,49 +15,28 @@ **********************************************************************/ require('admin.inc.php'); $errors=array(); -$SettingOptions=array('general'=>'General Settings', - 'dates'=>'Date and Time Options', - 'tickets'=>'Ticket Settings and Options', - 'emails'=>'Email Settings', - 'attachments'=>'Attachments Settings', - 'kb'=>'Knowledgebase Settings', - 'autoresponders'=>'Autoresponder Settings', - 'alerts'=>'Alerts and Notices Settings'); - +$settingOptions=array( + 'system' => 'System Settings', + 'tickets' => 'Ticket Settings and Options', + 'emails' => 'Email Settings', + 'kb' => 'Knowledgebase Settings', + 'autoresp' => 'Autoresponder Settings', + 'alerts' => 'Alerts and Notices Settings'); //Handle a POST. -if($_POST && !$errors){ - $errors=array(); - if($cfg && $cfg->updateSettings($_POST,$errors)){ - $msg=Format::htmlchars($SettingOptions[$_POST['t']]).' Updated Successfully'; +if($_POST && !$errors) { + if($cfg && $cfg->updateSettings($_POST,$errors)) { + $msg=Format::htmlchars($settingOptions[$_POST['t']]).' Updated Successfully'; $cfg->reload(); - }elseif(!$errors['err']){ - $errors['err']='Unable to update system settings - correct any errors below and try again'; + } elseif(!$errors['err']) { + $errors['err']='Unable to update settings - correct errors below and try again'; } } -$target=($_REQUEST['t'] && $SettingOptions[$_REQUEST['t']])?$_REQUEST['t']:'general'; - -$nav->setTabActive('settings'); -require(STAFFINC_DIR.'header.inc.php'); -?> -<h2>System Preferences and Settings - <span>osTicket (v<?php echo $cfg->getVersion(); ?>)</span></h2> -<div style="padding-top:10px;padding-bottom:5px;"> - <form method="get" action="settings.php"> - Setting Option: - <select id="setting_options" name="t" style="width:300px;"> - <option value="">— Select Setting Group —</option> - <?php - foreach($SettingOptions as $k=>$v) { - $sel=($target==$k)?'selected="selected"':''; - echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); - } - ?> - </select> - <input type="submit" value="Go"> - </form> -</div> -<?php +$target=($_REQUEST['t'] && $settingOptions[$_REQUEST['t']])?$_REQUEST['t']:'system'; $config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfigInfo()); + +$nav->setTabActive('settings', ('settings.php?t='.$target)); +require_once(STAFFINC_DIR.'header.inc.php'); include_once(STAFFINC_DIR."settings-$target.inc.php"); include_once(STAFFINC_DIR.'footer.inc.php'); ?> diff --git a/scp/slas.php b/scp/slas.php index 8f3b0f75ca331381beb6a348f46c2a703853b229..62f09c4da3c592255c634c1474260ae247403033 100644 --- a/scp/slas.php +++ b/scp/slas.php @@ -41,53 +41,56 @@ if($_POST){ break; case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { - $errors['err']='You must select at least one plan.'; - }else{ + $errors['err'] = 'You must select at least one plan.'; + } else { $count=count($_POST['ids']); - if($_POST['enable']){ - $sql='UPDATE '.SLA_TABLE.' SET isactive=1 WHERE id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())){ - if($num==$count) - $msg='Selected SLA plans enabled'; - else - $warn="$num of $count selected SLA plans enabled"; - }else{ - $errors['err']='Unable to enable selected SLA plans.'; - } - }elseif($_POST['disable']){ - $sql='UPDATE '.SLA_TABLE.' SET isactive=0 WHERE id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())) { - if($num==$count) - $msg='Selected SLA plans disabled'; - else - $warn="$num of $count selected SLA plans disabled"; - }else{ - $errors['err']='Unable to disable selected SLA plans'; - } - - }elseif($_POST['delete']){ - $i=0; - foreach($_POST['ids'] as $k=>$v) { - if(($p=SLA::lookup($v)) && $p->delete()) - $i++; - } - - if($i && $i==$count) - $msg='Selected SLA plans deleted successfully'; - elseif($i>0) - $warn="$i of $count selected SLA plans deleted"; - elseif(!$errors['err']) - $errors['err']='Unable to delete selected SLA plans'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.SLA_TABLE.' SET isactive=1 ' + .' WHERE id IN ('.implode(',', db_input($_POST['ids'])).')'; - }else { - $errors['err']='Unknown action'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected SLA plans enabled'; + else + $warn = "$num of $count selected SLA plans enabled"; + } else { + $errors['err'] = 'Unable to enable selected SLA plans.'; + } + break; + case 'disable': + $sql='UPDATE '.SLA_TABLE.' SET isactive=0 ' + .' WHERE id IN ('.implode(',', db_input($_POST['ids'])).')'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected SLA plans disabled'; + else + $warn = "$num of $count selected SLA plans disabled"; + } else { + $errors['err'] = 'Unable to disable selected SLA plans'; + } + break; + case 'delete': + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($p=SLA::lookup($v)) && $p->delete()) + $i++; + } + + if($i && $i==$count) + $msg = 'Selected SLA plans deleted successfully'; + elseif($i>0) + $warn = "$i of $count selected SLA plans deleted"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected SLA plans'; + break; + default: + $errors['err']='Unknown action - get technical help.'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown action/command'; break; } } @@ -96,7 +99,7 @@ $page='slaplans.inc.php'; if($sla || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='slaplan.inc.php'; -$nav->setTabActive('settings'); +$nav->setTabActive('manage'); require(STAFFINC_DIR.'header.inc.php'); require(STAFFINC_DIR.$page); include(STAFFINC_DIR.'footer.inc.php'); diff --git a/scp/staff.inc.php b/scp/staff.inc.php index 5dcf6045042397c1ee806a8df9f0f2b0f28725a3..8553a8bac1fef78482532de7789723af43e6c20d 100644 --- a/scp/staff.inc.php +++ b/scp/staff.inc.php @@ -49,7 +49,7 @@ require_once(INCLUDE_DIR.'class.csrf.php'); if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the function to trap expired sessions. function staffLoginPage($msg) { - $_SESSION['_staff']['auth']['dest']=THISPAGE; + $_SESSION['_staff']['auth']['dest']=THISURI; $_SESSION['_staff']['auth']['msg']=$msg; require(SCP_DIR.'login.php'); exit; @@ -122,4 +122,7 @@ if($thisstaff->forcePasswdChange() && !$exempt) { require('profile.php'); //profile.php must request this file as require_once to avoid problems. exit; } + +$ost->setPageTitle('osTicket :: Staff Control Panel'); + ?> diff --git a/scp/staff.php b/scp/staff.php index 88c8949f9908542067bf0ebc03e64781c5ce0b08..65f80dcc8056661de55b7f09d7ff2ea99b254eba 100644 --- a/scp/staff.php +++ b/scp/staff.php @@ -39,52 +39,59 @@ if($_POST){ break; case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { - $errors['err']='You must select at least one staff member.'; - }elseif(in_array($thisstaff->getId(),$_POST['ids'])) { - $errors['err']='You can not disable/delete yourself - you could be the only admin!'; - }else{ + $errors['err'] = 'You must select at least one staff member.'; + } elseif(in_array($thisstaff->getId(),$_POST['ids'])) { + $errors['err'] = 'You can not disable/delete yourself - you could be the only admin!'; + } else { $count=count($_POST['ids']); - if($_POST['enable']){ - $sql='UPDATE '.STAFF_TABLE.' SET isactive=1 WHERE staff_id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())){ - if($num==$count) - $msg='Selected staff activated'; - else - $warn="$num of $count selected staff activated"; - }else{ - $errors['err']='Unable to activate selected staff'; - } - }elseif($_POST['disable']){ - $sql='UPDATE '.STAFF_TABLE.' SET isactive=0 '. - 'WHERE staff_id IN ('.implode(',',$_POST['ids']).') AND staff_id!='.db_input($thisstaff->getId()); - if(db_query($sql) && ($num=db_affected_rows())) { - if($num==$count) - $msg='Selected staff disabled'; - else - $warn="$num of $count selected staff disabled"; - }else{ - $errors['err']='Unable to disable selected staff'; - } - }elseif($_POST['delete']){ - foreach($_POST['ids'] as $k=>$v) { - if($v!=$thisstaff->getId() && ($s=Staff::lookup($v)) && $s->delete()) - $i++; - } + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.STAFF_TABLE.' SET isactive=1 ' + .' WHERE staff_id IN ('.implode(',', db_input($_POST['ids'])).')'; - if($i && $i==$count) - $msg='Selected staff deleted successfully'; - elseif($i>0) - $warn="$i of $count selected staff deleted"; - elseif(!$errors['err']) - $errors['err']='Unable to delete selected staff.'; - }else{ - $errors['err']='Unknown action. Get technical help!'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected staff activated'; + else + $warn = "$num of $count selected staff activated"; + } else { + $errors['err'] = 'Unable to activate selected staff'; + } + break; + case 'disable': + $sql='UPDATE '.STAFF_TABLE.' SET isactive=0 ' + .' WHERE staff_id IN ('.implode(',',$_POST['ids']).') AND staff_id!='.db_input($thisstaff->getId()); + + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected staff disabled'; + else + $warn = "$num of $count selected staff disabled"; + } else { + $errors['err'] = 'Unable to disable selected staff'; + } + break; + case 'delete': + foreach($_POST['ids'] as $k=>$v) { + if($v!=$thisstaff->getId() && ($s=Staff::lookup($v)) && $s->delete()) + $i++; + } + + if($i && $i==$count) + $msg = 'Selected staff deleted successfully'; + elseif($i>0) + $warn = "$i of $count selected staff deleted"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected staff.'; + break; + default: + $errors['err'] = 'Unknown action. Get technical help!'; } + } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown action/command'; break; } } diff --git a/scp/teams.php b/scp/teams.php index 50d7ca1cae022dd328189b9d8ffdaaad1727c533..b8ebd4b280343641b66082d54058c0fc9971af5f 100644 --- a/scp/teams.php +++ b/scp/teams.php @@ -40,49 +40,54 @@ if($_POST){ case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { $errors['err']='You must select at least one team.'; - }else{ + } else { $count=count($_POST['ids']); - if($_POST['enable']){ - $sql='UPDATE '.TEAM_TABLE.' SET isenabled=1 WHERE team_id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())){ - if($num==$count) - $msg='Selected teams activated'; - else - $warn="$num of $count selected teams activated"; - }else{ - $errors['err']='Unable to activate selected teams'; - } - }elseif($_POST['disable']){ - $sql='UPDATE '.TEAM_TABLE.' SET isenabled=0 WHERE team_id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())) { - if($num==$count) - $msg='Selected teams disabled'; - else - $warn="$num of $count selected teams disabled"; - }else{ - $errors['err']='Unable to disable selected teams'; - } - }elseif($_POST['delete']){ - foreach($_POST['ids'] as $k=>$v) { - if(($t=Team::lookup($v)) && $t->delete()) - $i++; - } + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.TEAM_TABLE.' SET isenabled=1 ' + .' WHERE team_id IN ('.implode(',', db_input($_POST['ids'])).')'; - if($i && $i==$count) - $msg='Selected teams deleted successfully'; - elseif($i>0) - $warn="$i of $count selected teams deleted"; - elseif(!$errors['err']) - $errors['err']='Unable to delete selected teams'; - }else{ - $errors['err']='Unknown action. Get technical help!'; + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected teams activated'; + else + $warn = "$num of $count selected teams activated"; + } else { + $errors['err'] = 'Unable to activate selected teams'; + } + break; + case 'disable': + $sql='UPDATE '.TEAM_TABLE.' SET isenabled=0 ' + .' WHERE team_id IN ('.implode(',', db_input($_POST['ids'])).')'; + + if(db_query($sql) && ($num=db_affected_rows())) { + if($num==$count) + $msg = 'Selected teams disabled'; + else + $warn = "$num of $count selected teams disabled"; + } else { + $errors['err'] = 'Unable to disable selected teams'; + } + break; + case 'delete': + foreach($_POST['ids'] as $k=>$v) { + if(($t=Team::lookup($v)) && $t->delete()) + $i++; + } + if($i && $i==$count) + $msg = 'Selected teams deleted successfully'; + elseif($i>0) + $warn = "$i of $count selected teams deleted"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected teams'; + break; + default: + $errors['err'] = 'Unknown action. Get technical help!'; } } break; default: - $errors['err']='Unknown action'; + $errors['err']='Unknown action/command'; break; } } diff --git a/scp/templates.php b/scp/templates.php index 4ba95d70a8a707422bf011781b6df9ab292a5f9f..1c0db45b1a84b2dcce432ce66b7aa1e9f7e4cdc8 100644 --- a/scp/templates.php +++ b/scp/templates.php @@ -51,49 +51,50 @@ if($_POST){ case 'mass_process': if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { $errors['err']='You must select at least one template to process.'; - }else{ + } else { $count=count($_POST['ids']); - if($_POST['enable']){ - $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET isactive=1 WHERE tpl_id IN ('. - implode(',', db_input($_POST['ids'])).')'; - if(db_query($sql) && ($num=db_affected_rows())){ - if($num==$count) - $msg='Selected templates enabled'; + switch(strtolower($_POST['a'])) { + case 'enable': + $sql='UPDATE '.EMAIL_TEMPLATE_TABLE.' SET isactive=1 ' + .' WHERE tpl_id IN ('.implode(',', db_input($_POST['ids'])).')'; + if(db_query($sql) && ($num=db_affected_rows())){ + if($num==$count) + $msg = 'Selected templates enabled'; + else + $warn = "$num of $count selected templates enabled"; + } else { + $errors['err'] = 'Unable to enable selected templates'; + } + break; + case 'disable': + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=Template::lookup($v)) && !$t->isInUse() && $t->disable()) + $i++; + } + if($i && $i==$count) + $msg = 'Selected templates disabled'; + elseif($i) + $warn = "$i of $count selected templates disabled (in-use templates can't be disabled)"; else - $warn="$num of $count selected templates enabled"; - }else{ - $errors['err']='Unable to enable selected templates'; - } - }elseif($_POST['disable']){ + $errors['err'] = "Unable to disable selected templates (in-use or default template can't be disabled)"; + break; + case 'delete': + $i=0; + foreach($_POST['ids'] as $k=>$v) { + if(($t=Template::lookup($v)) && !$t->isInUse() && $t->delete()) + $i++; + } - $i=0; - foreach($_POST['ids'] as $k=>$v) { - if(($t=Template::lookup($v)) && !$t->isInUse() && $t->disable()) - $i++; - } - - if($i && $i==$count) - $msg='Selected templates disabled'; - elseif($i) - $warn="$i of $count selected templates disabled (in-use templates can't be disabled)"; - else - $errors['err']="Unable to disable selected templates (in-use or default template can't be disabled)"; - }elseif($_POST['delete']){ - $i=0; - foreach($_POST['ids'] as $k=>$v) { - if(($t=Template::lookup($v)) && $t->delete()) - $i++; - } - - if($i && $i==$count) - $msg='Selected templates deleted successfully'; - elseif($i>0) - $warn="$i of $count selected templates deleted"; - elseif(!$errors['err']) - $errors['err']='Unable to delete selected templates'; - - }else { - $errors['err']='Unknown template action'; + if($i && $i==$count) + $msg = 'Selected templates deleted successfully'; + elseif($i>0) + $warn = "$i of $count selected templates deleted"; + elseif(!$errors['err']) + $errors['err'] = 'Unable to delete selected templates'; + break; + default: + $errors['err']='Unknown template action'; } } break; diff --git a/scp/tickets.php b/scp/tickets.php index 8c799a2a6a02a26c4185afece329ffc5e57cdcf4..47395d592c2da9cb505daec7d037fb1349711dfa 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -52,109 +52,114 @@ if($_POST && !$errors): $errors['err']='Action Denied. Ticket is locked by someone else!'; //Make sure the email is not banned - if(!$errors['err'] && EmailFilter::isBanned($ticket->getEmail())) + if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail())) $errors['err']='Email is in banlist. Must be removed to reply.'; $wasOpen =($ticket->isOpen()); //If no error...do the do. - if(!$errors && ($respId=$ticket->postReply($_POST, $errors))) { + if(!$errors && ($respId=$ticket->postReply($_POST, $errorsi, isset($_POST['emailreply'])))) { $msg='Reply posted successfully'; $ticket->reload(); if($ticket->isClosed() && $wasOpen) $ticket=null; + } elseif(!$errors['err']) { $errors['err']='Unable to post the reply. Correct the errors below and try again!'; } break; case 'transfer': /** Transfer ticket **/ //Check permission - if($thisstaff && $thisstaff->canTransferTickets()) { + if(!$thisstaff->canTransferTickets()) + $errors['err']=$errors['transfer'] = 'Action Denied. You are not allowed to transfer tickets.'; + else { + + //Check target dept. if(!$_POST['deptId']) - $errors['deptId']='Select department'; + $errors['deptId'] = 'Select department'; elseif($_POST['deptId']==$ticket->getDeptId()) - $errors['deptId']='Ticket already in the Dept.'; + $errors['deptId'] = 'Ticket already in the department'; elseif(!($dept=Dept::lookup($_POST['deptId']))) - $errors['deptId']='Unknown or invalid department'; - - if(!$_POST['transfer_message']) - $errors['transfer_message'] = 'Transfer comments/notes required'; - elseif(strlen($_POST['transfer_message'])<5) - $errors['transfer_message'] = 'Transfer comments too short!'; - - $currentDept = $ticket->getDeptName(); //save current dept name. - if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_message'])) { + $errors['deptId'] = 'Unknown or invalid department'; + + //Transfer message - required. + if(!$_POST['transfer_comments']) + $errors['transfer_comments'] = 'Transfer comments required'; + elseif(strlen($_POST['transfer_comments'])<5) + $errors['transfer_comments'] = 'Transfer comments too short!'; + + //If no errors - them attempt the transfer. + if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_comments'])) { $msg = 'Ticket transferred successfully to '.$ticket->getDeptName(); - //ticket->transfer does a reload...new dept at this point. - $title='Dept. Transfer from '.$currentDept.' to '.$ticket->getDeptName(); - /*** log the message as internal note - with alerts disabled - ***/ - $ticket->postNote($title, $_POST['transfer_message'], false); //Check to make sure the staff still has access to the ticket if(!$ticket->checkStaffAccess($thisstaff)) $ticket=null; - } else { - $errors['transfer']='Unable to complete the transfer - try again'; - $errors['err']='Missing or invalid data. Correct the error(s) below and try again!'; + } elseif(!$errors['transfer']) { + $errors['err'] = 'Unable to complete the ticket transfer'; + $errors['transfer']='Correct the error(s) below and try again!'; } - } else { - $errors['err']=$errors['transfer']='Action Denied. You are not allowed to transfer tickets.'; } break; case 'assign': - if($thisstaff && $thisstaff->canAssignTickets()) { - if(!$_POST['assignId']) + if(!$thisstaff->canAssignTickets()) + $errors['err']=$errors['assign'] = 'Action Denied. You are not allowed to assign/reassign tickets.'; + else { + + $id = preg_replace("/[^0-9]/", "",$_POST['assignId']); + $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId()); + + if(!$_POST['assignId'] || !$id) $errors['assignId'] = 'Select assignee'; - elseif($_POST['assignId'][0]!='s' && $_POST['assignId'][0]!='t') + elseif($_POST['assignId'][0]!='s' && $_POST['assignId'][0]!='t' && !$claim) $errors['assignId']='Invalid assignee ID - get technical support'; elseif($ticket->isAssigned()) { - $id=preg_replace("/[^0-9]/", "",$_POST['assignId']); if($_POST['assignId'][0]=='s' && $id==$ticket->getStaffId()) $errors['assignId']='Ticket already assigned to the staff.'; elseif($_POST['assignId'][0]=='t' && $id==$ticket->getTeamId()) $errors['assignId']='Ticket already assigned to the team.'; } - if(!$_POST['assign_message']) - $errors['assign_message']='Comments required'; - elseif(strlen($_POST['assign_message'])<5) - $errors['assign_message']='Comments too short'; - - if(!$errors && $ticket->assign($_POST['assignId'],$_POST['assign_message'])) { - $msg='Ticket assigned successfully to '.$ticket->getAssignee(); - TicketLock::removeStaffLocks($thisstaff->getId(),$ticket->getId()); - $ticket=null; - }elseif(!$errors['err']) { - $errors['err']='Unable to assign the ticket. Correct the errors below and try again.'; + //Comments are not required on self-assignment (claim) + if($claim && !$_POST['assign_comments']) + $_POST['assign_comments'] = 'Ticket claimed by '.$thisstaff->getName(); + elseif(!$_POST['assign_comments']) + $errors['assign_comments'] = 'Assignment comments required'; + elseif(strlen($_POST['assign_comments'])<5) + $errors['assign_comments'] = 'Comment too short'; + + if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) { + if($claim) { + $msg = 'Ticket is NOW assigned to you!'; + } else { + $msg='Ticket assigned successfully to '.$ticket->getAssigned(); + TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId()); + $ticket=null; + } + } elseif(!$errors['assign']) { + $errors['err'] = 'Unable to complete the ticket assignment'; + $errors['assign'] = 'Correct the error(s) below and try again!'; } - - } else { - $errors['err']=$errors['assign']='Action Denied. You are not allowed to assign/reassign tickets.'; } break; case 'postnote': /* Post Internal Note */ - $fields=array(); - $fields['title'] = array('type'=>'string', 'required'=>1, 'error'=>'Title required'); - $fields['internal_note'] = array('type'=>'string', 'required'=>1, 'error'=>'Note message required'); - - if(!Validator::process($fields, $_POST, $errors) && !$errors['err']) - $errors['err']=$errors['note']='Missing or invalid data. Correct the error(s) below and try again!'; - - if(!$errors && ($noteId=$ticket->postNote($_POST['title'], $_POST['internal_note']))) { + //Make sure the staff can set desired state + if($_POST['state']) { + if($_POST['state']=='closed' && !$thisstaff->canCloseTickets()) + $errors['state'] = "You don't have permission to close tickets"; + elseif(in_array($_POST['state'], array('overdue', 'notdue', 'unassigned')) + && (!($dept=$ticket->getDept()) || !$dept->isManager($thisstaff))) + $errors['state'] = "You don't have permission to set the state"; + } + + $wasOpen = ($ticket->isOpen()); + if(($noteId=$ticket->postNote($_POST, $errors, $thisstaff))) { $msg='Internal note posted successfully'; - //Upload attachments IF ANY - TODO: validate attachment types?? - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) - $ticket->uploadAttachments($files,$noteId,'N'); - //Set state: Error on state change not critical! - if(isset($_POST['note_ticket_state']) && $_POST['note_ticket_state']) { - if($ticket->setState($_POST['note_ticket_state']) && $ticket->reload()) { - $msg.=' and state changed to '.strtoupper($_POST['note_ticket_state']); - if($ticket->isClosed()) - $ticket=null; //Going back to main listing. - } - } - } elseif(!$errors['note']) { - $errors['note']='Error(s) occurred. Unable to post the note.'; + if($wasOpen && $ticket->isClosed()) + $ticket = null; //Going back to main listing. + } else { + $errors['err'] = 'Unable to post internal note - missing or invalid data.'; + $errors['postnote'] = 'Unable to post the note. Correct the error(s) below and try again!'; } break; case 'edit': @@ -172,122 +177,141 @@ if($_POST && !$errors): } break; case 'process': - $isdeptmanager=($ticket->getDeptId()==$thisstaff->getDeptId())?true:false; switch(strtolower($_POST['do'])): - case 'change_priority': - if(!$thisstaff->canManageTickets() && !$thisstaff->isManager()){ - $errors['err']='Perm. Denied. You are not allowed to change ticket\'s priority'; - }elseif(!$_POST['ticket_priority'] or !is_numeric($_POST['ticket_priority'])){ - $errors['err']='You must select priority'; - } - if(!$errors){ - if($ticket->setPriority($_POST['ticket_priority'])){ - $msg='Priority Changed Successfully'; - $ticket->reload(); - $note='Ticket priority set to "'.$ticket->getPriority().'" by '.$thisstaff->getName(); - $ticket->logActivity('Priority Changed',$note); - }else{ - $errors['err']='Problems changing priority. Try again'; - } - } - break; case 'close': - if(!$thisstaff->isAdmin() && !$thisstaff->canCloseTickets()){ - $errors['err']='Perm. Denied. You are not allowed to close tickets.'; - }else{ - if($ticket->close()){ - $msg='Ticket #'.$ticket->getExtId().' status set to CLOSED'; - $note='Ticket closed without response by '.$thisstaff->getName(); - $ticket->logActivity('Ticket Closed',$note); - $page=$ticket=null; //Going back to main listing. - }else{ - $errors['err']='Problems closing the ticket. Try again'; - } + if(!$thisstaff->canCloseTickets()) { + $errors['err'] = 'Perm. Denied. You are not allowed to close tickets.'; + } elseif($ticket->isClosed()) { + $errors['err'] = 'Ticket is already closed!'; + } elseif($ticket->close()) { + $msg='Ticket #'.$ticket->getExtId().' status set to CLOSED'; + //Log internal note + if($_POST['ticket_status_notes']) + $note = $_POST['ticket_status_notes']; + else + $note='Ticket closed (without comments)'; + + $ticket->logNote('Ticket Closed', $note, $thisstaff); + + //Going back to main listing. + TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId()); + $page=$ticket=null; + + } else { + $errors['err']='Problems closing the ticket. Try again'; } break; case 'reopen': - //if they can close...then assume they can reopen. - if(!$thisstaff->isAdmin() && !$thisstaff->canCloseTickets()){ + //if staff can close or create tickets ...then assume they can reopen. + if(!$thisstaff->canCloseTickets() && !$thisstaff->canCreateTickets()) { $errors['err']='Perm. Denied. You are not allowed to reopen tickets.'; - }else{ - if($ticket->reopen()){ - $msg='Ticket status set to OPEN'; + } elseif($ticket->isOpen()) { + $errors['err'] = 'Ticket is already open!'; + } elseif($ticket->reopen()) { + $msg='Ticket REOPENED'; + + if($_POST['ticket_status_notes']) + $note = $_POST['ticket_status_notes']; + else $note='Ticket reopened (without comments)'; - if($_POST['ticket_priority']) { - $ticket->setPriority($_POST['ticket_priority']); - $ticket->reload(); - $note.=' and status set to '.$ticket->getPriority(); - } - $note.=' by '.$thisstaff->getName(); - $ticket->logActivity('Ticket Reopened',$note); - }else{ - $errors['err']='Problems reopening the ticket. Try again'; - } + + $ticket->logNote('Ticket Reopened', $note, $thisstaff); + + } else { + $errors['err']='Problems reopening the ticket. Try again'; } break; case 'release': - if(!($staff=$ticket->getStaff())) - $errors['err']='Ticket is not assigned!'; - elseif($ticket->release()) { - $msg='Ticket released (unassigned) from '.$staff->getName().' by '.$thisstaff->getName();; - $ticket->logActivity('Ticket unassigned',$msg); - }else - $errors['err']='Problems releasing the ticket. Try again'; + if(!$ticket->isAssigned() || !($assigned=$ticket->getAssigned())) { + $errors['err'] = 'Ticket is not assigned!'; + } elseif($ticket->release()) { + $msg='Ticket released (unassigned) from '.$assigned; + $ticket->logActivity('Ticket unassigned',$msg.' by '.$thisstaff->getName()); + } else { + $errors['err'] = 'Problems releasing the ticket. Try again'; + } + break; + case 'claim': + if(!$thisstaff->canAssignTickets()) { + $errors['err'] = 'Perm. Denied. You are not allowed to assign/claim tickets.'; + } elseif(!$ticket->isOpen()) { + $errors['err'] = 'Only open tickets can be assigned'; + } elseif($ticket->isAssigned()) { + $errors['err'] = 'Ticket is already assigned to '.$ticket->getAssigned(); + } elseif($ticket->assignToStaff($thisstaff->getId(), ('Ticket claimed by '.$thisstaff->getName()), false)) { + $msg = 'Ticket is now assigned to you!'; + } else { + $errors['err'] = 'Problems assigning the ticket. Try again'; + } break; case 'overdue': - //Mark the ticket as overdue - if(!$thisstaff->isAdmin() && !$thisstaff->isManager()){ + $dept = $ticket->getDept(); + if(!$dept || !$dept->isManager($thisstaff)) { $errors['err']='Perm. Denied. You are not allowed to flag tickets overdue'; - }else{ - if($ticket->markOverdue()){ - $msg='Ticket flagged as overdue'; - $note=$msg; - if($_POST['ticket_priority']) { - $ticket->setPriority($_POST['ticket_priority']); - $ticket->reload(); - $note.=' and status set to '.$ticket->getPriority(); - } - $note.=' by '.$thisstaff->getName(); - $ticket->logActivity('Ticket Marked Overdue',$note); - }else{ - $errors['err']='Problems marking the the ticket overdue. Try again'; - } + } elseif($ticket->markOverdue()) { + $msg='Ticket flagged as overdue'; + $ticket->logActivity('Ticket Marked Overdue',($msg.' by '.$thisstaff->getName())); + } else { + $errors['err']='Problems marking the the ticket overdue. Try again'; + } + break; + case 'answered': + $dept = $ticket->getDept(); + if(!$dept || !$dept->isManager($thisstaff)) { + $errors['err']='Perm. Denied. You are not allowed to flag tickets'; + } elseif($ticket->markAnswered()) { + $msg='Ticket flagged as answered'; + $ticket->logActivity('Ticket Marked Answered',($msg.' by '.$thisstaff->getName())); + } else { + $errors['err']='Problems marking the the ticket answered. Try again'; + } + break; + case 'unanswered': + $dept = $ticket->getDept(); + if(!$dept || !$dept->isManager($thisstaff)) { + $errors['err']='Perm. Denied. You are not allowed to flag tickets'; + } elseif($ticket->markUnAnswered()) { + $msg='Ticket flagged as unanswered'; + $ticket->logActivity('Ticket Marked Unanswered',($msg.' by '.$thisstaff->getName())); + } else { + $errors['err']='Problems marking the the ticket unanswered. Try again'; } break; case 'banemail': - if(!$thisstaff->isAdmin() && !$thisstaff->canBanEmails()){ + if(!$thisstaff->canBanEmails()) { $errors['err']='Perm. Denied. You are not allowed to ban emails'; - }elseif(Banlist::add($ticket->getEmail(),$thisstaff->getName())){ + } elseif(BanList::includes($ticket->getEmail())) { + $errors['err']='Email already in banlist'; + } elseif(Banlist::add($ticket->getEmail(),$thisstaff->getName())) { $msg='Email ('.$ticket->getEmail().') added to banlist'; - if($ticket->isOpen() && $ticket->close()) { - $msg.=' & ticket status set to closed'; - $ticket->logActivity('Ticket Closed',$msg); - $page=$ticket=null; //Going back to main listing. - } - }else{ + } else { $errors['err']='Unable to add the email to banlist'; } break; case 'unbanemail': - if(!$thisstaff->isAdmin() && !$thisstaff->canBanEmails()){ - $errors['err']='Perm. Denied. You are not allowed to remove emails from banlist.'; - }elseif(Banlist::remove($ticket->getEmail())){ - $msg='Email removed from banlist'; - }else{ + if(!$thisstaff->canBanEmails()) { + $errors['err'] = 'Perm. Denied. You are not allowed to remove emails from banlist.'; + } elseif(Banlist::remove($ticket->getEmail())) { + $msg = 'Email removed from banlist'; + } elseif(!BanList::includes($ticket->getEmail())) { + $warn = 'Email is not in the banlist'; + } else { $errors['err']='Unable to remove the email from banlist. Try again.'; } break; case 'delete': // Dude what are you trying to hide? bad customer support?? - if(!$thisstaff->isAdmin() && !$thisstaff->canDeleteTickets()){ + if(!$thisstaff->canDeleteTickets()) { $errors['err']='Perm. Denied. You are not allowed to DELETE tickets!!'; - }else{ - if($ticket->delete()){ - $page='tickets.inc.php'; //ticket is gone...go back to the listing. - $msg='Ticket Deleted Forever'; - $ticket=null; //clear the object. - }else{ - $errors['err']='Problems deleting the ticket. Try again'; - } + } elseif($ticket->delete()) { + $msg='Ticket #'.$ticket->getNumber().' deleted successfully'; + //Log a debug note + $ost->logDebug('Ticket #'.$ticket->getNumber().' deleted', + sprintf('Ticket #%s deleted by %s', + $ticket->getNumber(), $thisstaff->getName()) + ); + $ticket=null; //clear the object. + } else { + $errors['err']='Problems deleting the ticket. Try again'; } break; default: @@ -300,62 +324,99 @@ if($_POST && !$errors): if($ticket && is_object($ticket)) $ticket->reload();//Reload ticket info following post processing }elseif($_POST['a']) { + switch($_POST['a']) { case 'mass_process': if(!$thisstaff->canManageTickets()) $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access'; elseif(!$_POST['tids'] || !is_array($_POST['tids'])) $errors['err']='No tickets selected. You must select at least one ticket.'; - elseif(($_POST['reopen'] || $_POST['close']) && !$thisstaff->canCloseTickets()) - $errors['err']='You do not have permission to close/reopen tickets'; - elseif($_POST['delete'] && !$thisstaff->canDeleteTickets()) - $errors['err']='You do not have permission to delete tickets'; - elseif(!$_POST['tids'] || !is_array($_POST['tids'])) - $errors['err']='You must select at least one ticket'; - - if(!$errors) { + else { $count=count($_POST['tids']); - if(isset($_POST['reopen'])){ - $i=0; - $note='Ticket reopened by '.$thisstaff->getName(); - foreach($_POST['tids'] as $k=>$v) { - $t = new Ticket($v); - if($t && @$t->reopen()) { - $i++; - $t->logActivity('Ticket Reopened',$note,false,'System'); + $i = 0; + switch(strtolower($_POST['do'])) { + case 'reopen': + if($thisstaff->canCloseTickets() || $thisstaff->canCreateTickets()) { + $note='Ticket reopened by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + if(($t=Ticket::lookup($v)) && $t->isClosed() && @$t->reopen()) { + $i++; + $t->logNote('Ticket Reopened', $note); + } + } + + if($i==$count) + $msg = "Selected tickets ($i) reopened successfully"; + elseif($i) + $warn = "$i of $count selected tickets reopened"; + else + $errors['err'] = 'Unable to reopen selected tickets'; + } else { + $errors['err'] = 'You do not have permission to reopen tickets'; } - } - $msg="$i of $count selected tickets reopened"; - }elseif(isset($_POST['close'])){ - $i=0; - $note='Ticket closed without response by '.$thisstaff->getName(); - foreach($_POST['tids'] as $k=>$v) { - $t = new Ticket($v); - if($t && @$t->close()){ - $i++; - $t->logActivity('Ticket Closed',$note,false,'System'); + break; + case 'close': + if($thisstaff->canCloseTickets()) { + $note='Ticket closed without response by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + if(($t=Ticket::lookup($v)) && $t->isOpen() && @$t->close()) { + $i++; + $t->logNote('Ticket Closed', $note); + } + } + + if($i==$count) + $msg ="Selected tickets ($i) closed succesfully"; + elseif($i) + $warn = "$i of $count selected tickets closed"; + else + $errors['err'] = 'Unable to close selected tickets'; + } else { + $errors['err'] = 'You do not have permission to close tickets'; } - } - $msg="$i of $count selected tickets closed"; - }elseif(isset($_POST['overdue'])){ - $i=0; - $note='Ticket flagged as overdue by '.$thisstaff->getName(); - foreach($_POST['tids'] as $k=>$v) { - $t = new Ticket($v); - if($t && !$t->isOverdue()) - if($t->markOverdue()) { + break; + case 'mark_overdue': + $note='Ticket flagged as overdue by '.$thisstaff->getName(); + foreach($_POST['tids'] as $k=>$v) { + if(($t=Ticket::lookup($v)) && !$t->isOverdue() && $t->markOverdue()) { $i++; - $t->logActivity('Ticket Marked Overdue',$note,false,'System'); + $t->logNote('Ticket Marked Overdue', $note); } - } - $msg="$i of $count selected tickets marked overdue"; - }elseif(isset($_POST['delete'])){ - $i=0; - foreach($_POST['tids'] as $k=>$v) { - $t = new Ticket($v); - if($t && @$t->delete()) $i++; - } - $msg="$i of $count selected tickets deleted"; + } + + if($i==$count) + $msg = "Selected tickets ($i) marked overdue"; + elseif($i) + $warn = "$i of $count selected tickets marked overdue"; + else + $errors['err'] = 'Unable to flag selected tickets as overdue'; + break; + case 'delete': + if($thisstaff->canDeleteTickets()) { + foreach($_POST['tids'] as $k=>$v) { + if(($t=Ticket::lookup($v)) && @$t->delete()) $i++; + } + + //Log a warning + if($i) { + $log = sprintf('%s (%s) just deleted %d ticket(s)', + $thisstaff->getName(), $thisstaff->getUserName(), $i); + $ost->logWarning('Tickets deleted', $log, false); + + } + + if($i==$count) + $msg = "Selected tickets ($i) deleted successfully"; + elseif($i) + $warn = "$i of $count selected tickets deleted"; + else + $errors['err'] = 'Unable to delete selected tickets'; + } else { + $errors['err'] = 'You do not have permission to delete tickets'; + } + break; + default: + $errors['err']='Unknown or unsupported action - get technical help'; } } break; @@ -455,6 +516,7 @@ if($thisstaff->canCreateTickets()) { $inc = 'tickets.inc.php'; if($ticket) { + $ost->setPageTitle('Ticket #'.$ticket->getNumber()); $nav->setActiveSubMenu(-1); $inc = 'ticket-view.inc.php'; if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) @@ -481,7 +543,7 @@ if($ticket) { $nav->setActiveSubMenu(-1); //set refresh rate if the user has it configured - if(!$_POST && $_REQUEST['a']!='search' && ($min=$thisstaff->getRefreshRate())) + if(!$_POST && !$_REQUEST['a'] && ($min=$thisstaff->getRefreshRate())) $ost->addExtraHeader('<meta http-equiv="refresh" content="'.($min*60).'" />'); } diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/sql/osTicket-mysql.sql index 5cfba00d6538506e29e136f188610df4508551cb..5d2f4ceefdb5d912ef5cf7b84f9b1e3a91508d75 100644 --- a/setup/inc/sql/osTicket-mysql.sql +++ b/setup/inc/sql/osTicket-mysql.sql @@ -91,7 +91,7 @@ CREATE TABLE `%TABLE_PREFIX%config` ( `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_kb` tinyint(1) unsigned NOT NULL default '0', `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', @@ -228,14 +228,14 @@ CREATE TABLE `%TABLE_PREFIX%email` ( KEY `dept_id` (`dept_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%email_filter`; -CREATE TABLE `%TABLE_PREFIX%email_filter` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%filter`; +CREATE TABLE `%TABLE_PREFIX%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', + `reject_ticket` 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', `canned_response_id` int(11) unsigned NOT NULL default '0', @@ -245,21 +245,23 @@ CREATE TABLE `%TABLE_PREFIX%email_filter` ( `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', + `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any', `name` varchar(32) NOT NULL default '', `notes` text, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), + KEY `target` (`target`), KEY `email_id` (`email_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -INSERT INTO `%TABLE_PREFIX%email_filter` ( - `id`,`isactive`,`execorder`,`reject_email`,`name`,`notes`,`created`) +INSERT INTO `%TABLE_PREFIX%filter` ( + `id`,`isactive`,`execorder`,`reject_ticket`,`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` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`; +CREATE TABLE `%TABLE_PREFIX%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, @@ -274,7 +276,7 @@ CREATE TABLE `%TABLE_PREFIX%email_filter_rule` ( UNIQUE `filter` (`filter_id`, `what`, `how`, `val`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -INSERT INTO `%TABLE_PREFIX%email_filter_rule` ( +INSERT INTO `%TABLE_PREFIX%filter_rule` ( `id`, `filter_id`, `isactive`, `what`,`how`,`val`,`created`) VALUES (1, 1, 1, 'email', 'equal', 'test@example.com',NOW()); @@ -287,6 +289,8 @@ CREATE TABLE `%TABLE_PREFIX%email_template` ( `notes` text, `ticket_autoresp_subj` varchar(255) NOT NULL default '', `ticket_autoresp_body` text NOT NULL, + `ticket_autoreply_subj` varchar(255) NOT NULL default '', + `ticket_autoreply_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 '', @@ -315,8 +319,8 @@ CREATE TABLE `%TABLE_PREFIX%email_template` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- TODO: Dump revised copy before release!!! -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.', 'Open Tickets Limit Reached', '%name\r\n\r\nYou have reached the maximum number of open tickets allowed.\r\n\r\nTo 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'); +INSERT INTO `%TABLE_PREFIX%email_template` (`tpl_id`, `cfg_id`, `isactive`, `name`, `notes`, `ticket_autoresp_subj`, `ticket_autoresp_body`, `ticket_autoreply_subj`, `ticket_autoreply_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.number}]', '%{ticket.name},\r\n\r\nA request for support has been created and assigned ticket #%{ticket.number}. A representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %{ticket.client_link}.\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}', 'Support Ticket Opened [#%{ticket.number}]', '%{ticket.name},\r\n\r\nA request for support has been created and assigned ticket #%{ticket.number} with the following auto-reply:\r\n\r\n%{response}\r\n\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not open another ticket. If need be, representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %{ticket.client_link}.', '[#%{ticket.number}] %{ticket.subject}', '%{ticket.name},\r\n\r\nOur customer care team has created a ticket, #%{ticket.number} 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: %{ticket.client_link}.\r\n\r\n%{signature}', 'New Ticket Alert', '%{recipient},\r\n\r\nNew ticket #%{ticket.number} created.\r\n\r\n-----------------------\r\nName: %{ticket.name}\r\nEmail: %{ticket.email}\r\nDept: %{ticket.dept.name}\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%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', '[#%{ticket.number}] Message Added', '%{ticket.name},\r\n\r\nYour reply to support request #%{ticket.number} has been noted.\r\n\r\nYou can view this support request progress online here: %{ticket.client_link}.\r\n\r\n%{signature}', 'New Message Alert', '%{recipient},\r\n\r\nNew message appended to ticket #%{ticket.number}\r\n\r\n----------------------\r\nName: %{ticket.name}\r\nEmail: %{ticket.email}\r\nDept: %{ticket.dept.name}\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%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'New Internal Note Alert', '%{recipient},\r\n\r\nInternal note appended to ticket #%{ticket.number}\r\n\r\n----------------------\r\n* %{note.title} *\r\n\r\n%{note.message}\r\n----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Ticket #%{ticket.number} Assigned to you', '%{assignee},\r\n\r\nTicket #%{ticket.number} has been assigned to you by %{assigner}\r\n\r\n----------------------\r\n\r\n%{comments}\r\n\r\n----------------------\r\n\r\nTo view complete details, simply login to the support system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Support Ticket System - powered by osTicket.', 'Ticket Transfer #%{ticket.number} - %{ticket.dept.name}', '%{recipient},\r\n\r\nTicket #%{ticket.number} has been transferred to %{ticket.dept.name} department by %{staff.name}\r\n\r\n----------------------\r\n\r\n%{comments}\r\n\r\n----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Stale Ticket Alert', '%{recipient},\r\n\r\nA ticket, #%{ticket.number} assigned to you or in your department is seriously overdue.\r\n\r\n%{ticket.staff_link}\r\n\r\nWe should all work hard to guarantee that all tickets are being addressed in a timely manner.\r\n\r\n- Your friendly (although with limited patience) Support Ticket System - powered by osTicket.', 'Open Tickets Limit Reached', '%{ticket.name}\r\n\r\nYou have reached the maximum number of open tickets allowed.\r\n\r\nTo 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}/tickets.php?e=%{ticket.email}\r\n\r\nThank you.\r\n\r\nSupport Ticket System', '[#%{ticket.number}] %{ticket.subject}', '%{ticket.name},\r\n\r\nA customer support staff member has replied to your support request, #%{ticket.number} 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%{ticket.client_link}\r\n\r\n%{signature}', NOW(), NOW()); DROP TABLE IF EXISTS `%TABLE_PREFIX%file`; CREATE TABLE `%TABLE_PREFIX%file` ( @@ -325,15 +329,24 @@ CREATE TABLE `%TABLE_PREFIX%file` ( `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()); +INSERT INTO `%TABLE_PREFIX%file` (`id`, `type`, `size`, `hash`, `name`, `created`) VALUES +(1, 'text/plain', '25', '670c6cc1d1dfc97fad20e5470251b255', 'osTicket.txt', NOW()); +DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`; +CREATE TABLE `%TABLE_PREFIX%file_chunk` ( + `file_id` int(11) NOT NULL, + `chunk_id` int(11) NOT NULL, + `filedata` longblob NOT NULL, + PRIMARY KEY (`file_id`, `chunk_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`) +VALUES (1, 0, 0x43616e6e6564206174746163686d656e747320726f636b210a); DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`; CREATE TABLE `%TABLE_PREFIX%groups` ( @@ -420,7 +433,7 @@ CREATE TABLE `%TABLE_PREFIX%canned_response` ( 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'); + (2, 0, 1, 'Sample (with variables)', '\r\n%{ticket.name},\r\n\r\nYour ticket #%{ticket.number} created on %{ticket.create_date} is in %{ticket.dept.name} department.\r\n\r\n'); DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_attachment`; CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%canned_attachment` ( diff --git a/setup/inc/sql/osTicket-mysql.sql.md5 b/setup/inc/sql/osTicket-mysql.sql.md5 index 63d2ce443b0448ca776ebc37b298a3c4ff93c387..4aa4254e6af1e5496dc39cf89726de1a607bab14 100644 --- a/setup/inc/sql/osTicket-mysql.sql.md5 +++ b/setup/inc/sql/osTicket-mysql.sql.md5 @@ -1 +1 @@ -d0e37dca324648f1ce2d10528a6026d4 +00ff231f2ade8797a0e7f2a7fccd52f4 diff --git a/setup/setup.inc.php b/setup/setup.inc.php index 07b86c6ffa43e13338f3b1f6c176e0cee2f53316..a79c93c7e55ed3aab9a220ea54f1d558258e3fc6 100644 --- a/setup/setup.inc.php +++ b/setup/setup.inc.php @@ -15,10 +15,16 @@ **********************************************************************/ #This version - changed on every release -define('THIS_VERSION', '1.7-RC2'); +define('THIS_VERSION', '1.7-RC3'); -#inits -error_reporting(E_ALL ^ E_NOTICE); //turn on errors?? +#inits - error reporting. +$error_reporting = E_ALL & ~E_NOTICE; +if (defined('E_STRICT')) # 5.4.0 + $error_reporting &= ~E_STRICT; +if (defined('E_DEPRECATED')) # 5.3.0 + $error_reporting &= ~(E_DEPRECATED | E_USER_DEPRECATED); + +error_reporting($error_reporting); ini_set('magic_quotes_gpc', 0); ini_set('session.use_trans_sid', 0); ini_set('session.cache_limiter', 'nocache'); diff --git a/view.php b/view.php index 984b04c3019645c6ca1d075bbab0aac8460e4094..10e5374fe71b8dcf3551c0ee922ac5b4800e961a 100644 --- a/view.php +++ b/view.php @@ -3,6 +3,7 @@ view.php Ticket View. + TODO: Support different views based on auth_token - e.g for BCC'ed users vs. Ticket owner. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2010 osTicket @@ -14,8 +15,22 @@ vim: expandtab sw=4 ts=4 sts=4: $Id: $ **********************************************************************/ -require('secure.inc.php'); -if(!is_object($thisclient) || !$thisclient->isValid()) die('Access denied'); //Double check again. -//We are now using tickets.php but we need to keep view.php for backward compatibility +require_once('client.inc.php'); + +//If the user is NOT logged in - try auto-login (if params exists). +if(!$thisclient || !$thisclient->isValid()) { + // * On login Client::login will redirect the user to tickets.php view. + // * See TODO above for planned multi-view. + $user = null; + if($_GET['t'] && $_GET['e'] && $_GET['a']) + $user = Client::login($_GET['t'], $_GET['e'], $_GET['a'], $errors); + + //XXX: For now we're assuming the user is the ticket owner + // (multi-view based on auth token will come later). + if($user && $user->getTicketID()==trim($_GET['t'])) + @header('Location: tickets.php?id='.$user->getTicketID()); +} + +//Simply redirecting to tickets.php until multiview is implemented. require('tickets.php'); ?>