diff --git a/include/class.config.php b/include/class.config.php index c1d1ce691078ad0109e7dfb849d36e036e119e44..a1dd1de245f0a5245396a2cf81369ced37f1e8eb 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -856,6 +856,20 @@ class OsticketConfig extends Config { )); } + function getLogo($site) { + $id = $this->get("{$site}_logo_id", false); + return ($id) ? AttachmentFile::lookup($id) : null; + } + function getClientLogo() { + return $this->getLogo('client'); + } + function getLogoId($site) { + return $this->get("{$site}_logo_id", false); + } + function getClientLogoId() { + return $this->getLogoId('client'); + } + function updatePagesSettings($vars, &$errors) { $f=array(); @@ -863,13 +877,33 @@ class OsticketConfig extends Config { $f['offline_page_id'] = array('type'=>'int', 'required'=>1, 'error'=>'required'); $f['thank-you_page_id'] = array('type'=>'int', 'required'=>1, 'error'=>'required'); + if ($_FILES['logo']) { + $error = false; + list($logo) = AttachmentFile::format($_FILES['logo']); + if (!$logo) + ; // Pass + elseif ($logo['error']) + $errors['logo'] = $logo['error']; + elseif (!($id = AttachmentFile::uploadLogo($logo, $error))) + $errors['logo'] = 'Unable to upload logo image. '.$error; + } + if(!Validator::process($f, $vars, $errors) || $errors) return false; + if (isset($vars['delete-logo'])) + foreach ($vars['delete-logo'] as $id) + if (($vars['selected-logo'] != $id) + && ($f = AttachmentFile::lookup($id))) + $f->delete(); + return $this->updateAll(array( 'landing_page_id' => $vars['landing_page_id'], 'offline_page_id' => $vars['offline_page_id'], 'thank-you_page_id' => $vars['thank-you_page_id'], + 'client_logo_id' => ( + (is_numeric($vars['selected-logo']) && $vars['selected-logo']) + ? $vars['selected-logo'] : false), )); } diff --git a/include/class.file.php b/include/class.file.php index dcfc1e8d1d8b95d91079318adce5630dc77ef5cf..d4ea01f33fafa1b7a1e6bf0aa3d4076121dd7821 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -91,6 +91,18 @@ class AttachmentFile { return $this->ht['hash']; } + function lastModified() { + return $this->ht['created']; + } + + /** + * Retrieve a hash that can be sent to scp/file.php?h= in order to + * download this file + */ + function getDownloadHash() { + return strtolower($this->getHash() . md5($this->getId().session_id().$this->getHash())); + } + function open() { return new AttachmentChunkedData($this->id); } @@ -126,7 +138,19 @@ class AttachmentFile { function display() { - + + // Thanks, http://stackoverflow.com/a/1583753/1025836 + $last_modified = strtotime($this->lastModified()); + header("Last-Modified: ".gmdate(DATE_RFC822, $last_modified)." GMT", false); + header('ETag: "'.$this->getHash().'"'); + header('Cache-Control: private, max-age=3600'); + header('Expires: ' . date(DATE_RFC822, time() + 3600) . ' GMT'); + header('Pragma: private'); + if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified || + @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $this->getHash()) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream')); header('Content-Length: '.$this->getSize()); @@ -141,7 +165,7 @@ class AttachmentFile { header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Cache-Control: public'); header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream')); - + $filename=basename($this->getName()); $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']); if ((is_integer(strpos($user_agent,'msie'))) && (is_integer(strpos($user_agent,'win')))) { @@ -149,7 +173,7 @@ class AttachmentFile { }else{ header('Content-Disposition: attachment; filename='.$filename.';' ); } - + header('Content-Transfer-Encoding: binary'); header('Content-Length: '.$this->getSize()); $this->sendData(); @@ -157,12 +181,13 @@ class AttachmentFile { } /* Function assumes the files types have been validated */ - function upload($file) { - + function upload($file, $ft='T') { + if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name'])) return false; $info=array('type'=>$file['type'], + 'filetype'=>$ft, 'size'=>$file['size'], 'name'=>$file['name'], 'hash'=>MD5(MD5_FILE($file['tmp_name']).time()), @@ -172,15 +197,49 @@ class AttachmentFile { return AttachmentFile::save($info); } + function uploadLogo($file, &$error, $aspect_ratio=3) { + /* Borrowed in part from + * http://salman-w.blogspot.com/2009/04/crop-to-fit-image-using-aspphp.html + */ + if (!extension_loaded('gd')) + return self::upload($file, 'L'); + + $source_path = $file['tmp_name']; + + list($source_width, $source_height, $source_type) = getimagesize($source_path); + + switch ($source_type) { + case IMAGETYPE_GIF: + case IMAGETYPE_JPEG: + case IMAGETYPE_PNG: + break; + default: + // TODO: Return an error + $error = 'Invalid image file type'; + return false; + } + + $source_aspect_ratio = $source_width / $source_height; + + if ($source_aspect_ratio >= $aspect_ratio) + return self::upload($file, 'L'); + + $error = 'Image is too square. Upload a wider image'; + return false; + } + function save($file) { if(!$file['hash']) $file['hash']=MD5(MD5($file['data']).time()); if(!$file['size']) $file['size']=strlen($file['data']); - + if(!$file['filetype']) + $file['filetype'] = 'T'; + $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() ' .',type='.db_input($file['type']) + .',ft='.db_input($file['filetype']) .',size='.db_input($file['size']) .',name='.db_input(Format::file_name($file['name'])) .',hash='.db_input($file['hash']); @@ -208,11 +267,11 @@ class AttachmentFile { function lookup($id) { $id = is_numeric($id)?$id:AttachmentFile::getIdByHash($id); - + return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null; } - /* + /* Method formats http based $_FILE uploads - plus basic validation. @restrict - make sure file type & size are allowed. */ @@ -234,7 +293,7 @@ class AttachmentFile { foreach($attachments as $i => &$file) { //skip no file upload "error" - why PHP calls it an error is beyond me. if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) { - unset($attachments[$i]); + unset($attachments[$i]); continue; } @@ -263,7 +322,7 @@ class AttachmentFile { * canned-response, or faq point to any more. */ /* static */ function deleteOrphans() { - + $sql = 'DELETE FROM '.FILE_TABLE.' WHERE id NOT IN (' # DISTINCT implies sort and may not be necessary .'SELECT DISTINCT(file_id) FROM (' @@ -273,16 +332,27 @@ class AttachmentFile { .' UNION ALL ' .'SELECT file_id FROM '.FAQ_ATTACHMENT_TABLE .') still_loved' - .')'; + .') AND `ft` = "T"'; db_query($sql); - + //Delete orphaned chuncked data! AttachmentChunkedData::deleteOrphans(); - + return true; } + + /* static */ + function allLogos() { + $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE ft="L" + ORDER BY created'; + $logos = array(); + $res = db_query($sql); + while (list($id) = db_fetch_row($res)) + $logos[] = AttachmentFile::lookup($id); + return $logos; + } } /** @@ -328,11 +398,11 @@ class AttachmentChunkedData { } 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/client/header.inc.php b/include/client/header.inc.php index d4c90ff1fcec0a0bb4ba179dd675c738cbc5a06e..781df6e59568d7b20e3c926c046f6ea5214b7ae5 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -20,7 +20,10 @@ header("Content-Type: text/html; charset=UTF-8\r\n"); <body> <div id="container"> <div id="header"> - <a id="logo" href="<?php echo ROOT_PATH; ?>index.php" title="Support Center"><img src="<?php echo ASSETS_PATH; ?>images/logo.png" border=0 alt="Support Center"></a> + <a id="logo" href="<?php echo ROOT_PATH; ?>index.php" + title="Support Center"><img src="logo.php" border=0 alt="<?php + echo $ost->getConfig()->getTitle(); ?>" + style="height: 5em"></a> <p> <?php if($thisclient && is_object($thisclient) && $thisclient->isValid()) { diff --git a/include/staff/settings-pages.inc.php b/include/staff/settings-pages.inc.php index b58a148d4ae6173fc7ed9f688ba302dd0aa2f23e..4be25699801138abf371d387138a1d6e2918b03b 100644 --- a/include/staff/settings-pages.inc.php +++ b/include/staff/settings-pages.inc.php @@ -3,7 +3,8 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) $pages = Page::getPages(); ?> <h2>Site Pages</h2> -<form action="settings.php?t=pages" method="post" id="save"> +<form action="settings.php?t=pages" method="post" id="save" + enctype="multipart/form-data"> <?php csrf_token(); ?> <input type="hidden" name="t" value="pages" > <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> @@ -66,8 +67,117 @@ $pages = Page::getPages(); </tr> </tbody> </table> +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Logos</h4> + <em>System Default Logo</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td colspan="2"> + <label style="display:block"> + <input type="radio" name="selected-logo" value="0" + style="margin-left: 1em" + <?php if (!$ost->getConfig()->getClientLogoId()) + echo 'checked="checked"'; ?>/> + <img src="../assets/default/images/logo.png" + alt="Default Logo" valign="middle" + style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5); + margin: 0.5em; height: 5em"/> + </label> + </td></tr> + <tr> + <th colspan="2"> + <em>Use a custom logo — Use a delete checkbox to + remove the logo from the system</em> + </th> + </tr> + <tr><td colspan="2"> + <?php + $current = $ost->getConfig()->getClientLogoId(); + foreach (AttachmentFile::allLogos() as $logo) { ?> + <div> + <label> + <input type="radio" name="selected-logo" + style="margin-left: 1em" value="<?php + echo $logo->getId(); ?>" <?php + if ($logo->getId() == $current) + echo 'checked="checked"'; ?>/> + <img src="image.php?h=<?php echo $logo->getDownloadHash(); ?>" + alt="Custom Logo" valign="middle" + style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5); + margin: 0.5em; height: 5em;"/> + </label> + <?php if ($logo->getId() != $current) { ?> + <label> + <input type="checkbox" name="delete-logo[]" value="<?php + echo $logo->getId(); ?>"/> Delete + </label> + <?php } ?> + </div> + <?php } ?> + <br/> + <b>Upload a new logo:</b> + <input type="file" name="logo[]" size="30" value="" /> + <font class="error"><br/><?php echo $errors['logo']; ?></font> + </td> + </tr> + </tbody> +</table> <p style="padding-left:250px;"> - <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="submit" name="submit-button" value="Save Changes"> <input class="button" type="reset" name="reset" value="Reset Changes"> </p> </form> + +<div style="display:none;" class="dialog" id="confirm-action"> + <h3>Please Confirm</h3> + <a class="close" href="">×</a> + <hr/> + <p class="confirm-action" id="delete-confirm"> + <font color="red"><strong>Are you sure you want to DELETE selected + logos?</strong></font> + <br/><br/>Deleted logos 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> + +<script type="text/javascript"> +$(function() { + $('#save input:submit.button').bind('click', function(e) { + var formObj = $('#save'); + if ($('input:checkbox:checked', formObj).length) { + e.preventDefault(); + $('.dialog#confirm-action').undelegate('.confirm'); + $('.dialog#confirm-action').delegate('input.confirm', 'click', function(e) { + e.preventDefault(); + $('.dialog#confirm-action').hide(); + $('#overlay').hide(); + formObj.submit(); + return false; + }); + $('#overlay').show(); + $('.dialog#confirm-action .confirm-action').hide(); + $('.dialog#confirm-action p#delete-confirm') + .show() + .parent('div').show().trigger('click'); + return false; + } + else return true; + }); +}); +</script> diff --git a/login.php b/login.php index 8ca3901055291f961390319c0ca54e99c9d29add..789938980ff451cdcc72920d2b9f88080dcc7a47 100644 --- a/login.php +++ b/login.php @@ -2,7 +2,7 @@ /********************************************************************* login.php - Client Login + Client Login Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket diff --git a/logo.php b/logo.php new file mode 100644 index 0000000000000000000000000000000000000000..7972433548d8b6165aa31938a51911ce2e8683b1 --- /dev/null +++ b/logo.php @@ -0,0 +1,32 @@ +<?php +/********************************************************************* + logo.php + + Simple logo to facilitate serving a customized client-side logo from + osTicet. The logo is configurable in Admin Panel -> Settings -> Pages + + Peter Rotich <peter@osticket.com> + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 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: +**********************************************************************/ + +// Don't update the session for inline image fetches +if (!function_exists('noop')) { function noop() {} } +session_set_save_handler('noop','noop','noop','noop','noop','noop'); +define('DISABLE_SESSION', true); + +require('client.inc.php'); + +if (($logo = $ost->getConfig()->getClientLogo())) { + $logo->display(); +} else { + header('Location: '.ASSETS_PATH.'images/logo.png'); +} + +?> diff --git a/scp/image.php b/scp/image.php new file mode 100644 index 0000000000000000000000000000000000000000..089625ca1b340ff5718be52bdb31cc4b43c579ea --- /dev/null +++ b/scp/image.php @@ -0,0 +1,31 @@ +<?php +/********************************************************************* + image.php + + Simply downloads the file...on hash validation as follows; + + * Hash must be 64 chars long. + * First 32 chars is the perm. file hash + * Next 32 chars is md5(file_id.session_id().file_hash) + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require('staff.inc.php'); +require_once(INCLUDE_DIR.'class.file.php'); +$h=trim($_GET['h']); +//basic checks +if(!$h || strlen($h)!=64 //32*2 + || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. + || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash. + die('Unknown or invalid file. #'.Format::htmlchars($_GET['h'])); + +$file->display(); +?> diff --git a/secure.inc.php b/secure.inc.php index 7b94ff92f2b10618de9a2921773c098f475ee4d8..bf6a75b3e032590ca6ec89346e50c67393fd5514 100644 --- a/secure.inc.php +++ b/secure.inc.php @@ -20,6 +20,7 @@ require_once('client.inc.php'); //Client Login page: Ajax interface can pre-declare the function to trap logins. if(!function_exists('clientLoginPage')) { function clientLoginPage($msg ='') { + global $ost; require('./login.php'); exit; }