From 6dc05fbad45d89d10b6cdc5517739cd68aa50fba Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Tue, 16 Jul 2013 22:56:24 +0000
Subject: [PATCH] Allow custom logo integration on client site

Administrators are allowed to upload one or more logos and then select from
the uploaded logos to set one for the client site. Logos can also be deleted
on settings->pages submission
---
 include/class.config.php             |  34 ++++++++
 include/class.file.php               | 100 +++++++++++++++++++----
 include/client/header.inc.php        |   5 +-
 include/staff/settings-pages.inc.php | 114 ++++++++++++++++++++++++++-
 login.php                            |   2 +-
 logo.php                             |  32 ++++++++
 scp/image.php                        |  31 ++++++++
 secure.inc.php                       |   1 +
 8 files changed, 300 insertions(+), 19 deletions(-)
 create mode 100644 logo.php
 create mode 100644 scp/image.php

diff --git a/include/class.config.php b/include/class.config.php
index c1d1ce691..a1dd1de24 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 dcfc1e8d1..d4ea01f33 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 d4c90ff1f..781df6e59 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 b58a148d4..4be256998 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 &mdash; 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="">&times;</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 8ca390105..789938980 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 000000000..797243354
--- /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 000000000..089625ca1
--- /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 7b94ff92f..bf6a75b3e 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;
     }
-- 
GitLab