From 35acdea8d52c160b7c2d2bc9ca8fe2e774c691d1 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Mon, 20 Jul 2015 14:17:46 -0500
Subject: [PATCH] Add support for session-backed messages

This allows messages to be stashed in the session and displayed on a
following request. This will be pivotal in implementing the PRG
(post-redirect-get).
---
 include/class.config.php     |  11 +-
 include/class.message.php    | 248 +++++++++++++++++++++++++++++++++++
 include/class.orm.php        |  20 +--
 include/class.osticket.php   |   1 +
 include/class.ostsession.php |  13 +-
 include/class.util.php       |   4 +-
 include/staff/header.inc.php |   6 +-
 scp/css/scp.css              |   1 +
 scp/lists.php                |   3 +-
 9 files changed, 282 insertions(+), 25 deletions(-)
 create mode 100644 include/class.message.php

diff --git a/include/class.config.php b/include/class.config.php
index c9125aaa3..73890ea40 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -36,9 +36,8 @@ class Config {
         if ($this->section === null)
             return false;
 
-        if (!isset($_SESSION['cfg:'.$this->section]))
-            $_SESSION['cfg:'.$this->section] = array();
-        $this->session = &$_SESSION['cfg:'.$this->section];
+        if (isset($_SESSION['cfg:'.$this->section]))
+            $this->session = &$_SESSION['cfg:'.$this->section];
         $this->load();
     }
 
@@ -64,7 +63,7 @@ class Config {
     }
 
     function get($key, $default=null) {
-        if (isset($this->session[$key]))
+        if (isset($this->session) && isset($this->session[$key]))
             return $this->session[$key];
         elseif (isset($this->config[$key]))
             return $this->config[$key]['value'];
@@ -83,6 +82,10 @@ class Config {
     }
 
     function persist($key, $value) {
+        if (!isset($this->session)) {
+            $this->session = &$_SESSION['cfg:'.$this->section];
+            $this->session = array();
+        }
         $this->session[$key] = $value;
         return true;
     }
diff --git a/include/class.message.php b/include/class.message.php
new file mode 100644
index 000000000..e78de545f
--- /dev/null
+++ b/include/class.message.php
@@ -0,0 +1,248 @@
+<?php
+/*********************************************************************
+    class.message.php
+
+    Simple messages interface used to stash messages for display in a future
+    request. Mainly useful for the post-redirect-get pattern.
+
+    Usage:
+
+    <?php Messages::success('It worked!!'); ?>
+
+    // In a later request
+    <?php
+    foreach (Messages::getMessages() as $msg) {
+        include 'path/to/message-template.tmp.php';
+    }
+    ?>
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2015 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:
+**********************************************************************/
+
+interface Message {
+    function getTags();
+    function getLevel();
+    function __toString();
+}
+
+class Messages {
+    const ERROR = 50;
+    const WARNING = 40;
+    const WARN = self::WARNING;
+    const SUCCESS = 30;
+    const INFO = 20;
+    const DEBUG = 10;
+    const NOTSET = 0;
+
+    static $_levelNames = array(
+        self::ERROR => 'ERROR',
+        self::WARNING => 'WARNING',
+        self::SUCCESS => 'SUCCESS',
+        self::INFO => 'INFO',
+        self::DEBUG => 'DEBUG',
+        self::NOTSET => 'NOTSET',
+        'ERROR' => self::ERROR,
+        'WARN' => self::WARNING,
+        'WARNING' => self::WARNING,
+        'SUCCESS' => self::SUCCESS,
+        'INFO' => self::INFO,
+        'DEBUG' => self::DEBUG,
+        'NOTSET' => self::NOTSET,
+    );
+
+    static $messageClass = 'SimpleMessage';
+    static $backend = 'SessionMessageStorage';
+
+    static function debug($message) {
+        static::addMessage(self::DEBUG, $message);
+    }
+    static function info($message) {
+        static::addMessage(self::INFO, $message);
+    }
+    static function success($message) {
+        static::addMessage(self::SUCCESS, $message);
+    }
+    static function warning($message) {
+        static::addMessage(self::WARNING, $message);
+    }
+    static function error($message) {
+        static::addMessage(self::ERROR, $message);
+    }
+
+    static function addMessage($level, $message) {
+        $msg = new static::$messageClass($level, $message);
+        $bk = static::getMessages();
+        $bk->add($level, $msg);
+    }
+
+    static function getMessages() {
+        static $messages;
+        if (!isset($messages))
+            $messages = new static::$backend();
+        return $messages;
+    }
+
+    static function setMessageClass($class) {
+        if (!is_subclass_of($class, 'Message'))
+            throw new InvalidArgumentException('Class must extend Message');
+        self::$messageClass = $class;
+    }
+
+    static function checkLevel($level) {
+        if (is_int($level)) {
+            $rv = $level;
+        }
+        elseif ((string) $level == $level) {
+            if (!isset(static::$_levelNames[$level]))
+                throw new InvalidArgumentException(
+                    sprintf('Unknown level: %s', $level));
+            $rv = static::$_levelNames[$level];
+        }
+        else {
+            throw new InvalidArgumentException(
+                sprintf('Level not an integer or a valid string: %s',
+                    $level));
+        }
+        return $rv;
+    }
+
+    static function getLevelName($level) {
+        return @self::$_levelNames[$level];
+    }
+}
+
+class SimpleMessage implements Message {
+    var $tags;
+    var $level;
+    var $msg;
+
+    function __construct($level, $message, $extra_tags=array()) {
+        $this->level = $level;
+        $this->msg = $message;
+        $this->tags = $extra_tags ?: null;
+    }
+
+    function getTags() {
+        $tags = array_merge(
+            array(Messages::getLevelTag($this->level)),
+            $this->tags ?: array());
+        return implode(' ', $tags);
+    }
+
+    function getLevel() {
+        return Messages::getLevelName($this->level);
+    }
+
+    function __toString() {
+        return $this->msg;
+    }
+}
+
+interface MessageStorageBackend extends \IteratorAggregate {
+    function setLevel($level);
+    function getLevel();
+
+    function update();
+    function add($level, $message);
+}
+
+abstract class BaseMessageStorage implements MessageStorageBackend {
+    var $level = Messages::NOTSET;
+    var $queued = array();
+    var $used = false;
+    var $added_new = false;
+
+    function isEnabledFor($level) {
+        Messages::checkLevel($level);
+        return $level >= $this->getLevel();
+    }
+
+    function setLevel($level) {
+        Messages::checkLevel($level);
+        $this->level = $level;
+    }
+
+    function getLevel() {
+        return $this->level;
+    }
+
+    function load() {
+        static $messages = false;
+
+        if (!$messages) {
+            $messages = new ListObject($this->get());
+        }
+        return $messages;
+    }
+
+    function getIterator() {
+        $this->used = true;
+        $messages = $this->load();
+        if ($this->queued) {
+            $messages->extend($this->queued);
+            $this->queued = array();
+        }
+        if ($messages instanceof ListObject)
+            return $messages->getIterator();
+        else
+            return new \ArrayIterator($messages);
+    }
+
+    function update() {
+        if ($this->used) {
+            return $this->store($this->queued);
+        }
+        else {
+            $messages = $this->load();
+            $messages->extend($this->queued);
+            return $this->store($messages);
+        }
+    }
+
+    function add($level, $message) {
+        if (!$message)
+            return;
+        elseif (!$this->isEnabledFor($level))
+            return;
+
+        $this->added_new = true;
+        $this->queued[] = $message;
+    }
+
+    abstract function get();
+    abstract function store($messages);
+}
+
+class SessionMessageStorage extends BaseMessageStorage {
+    var $list;
+
+    function __construct() {
+        $this->list = @$_SESSION[':msgs'] ?: array();
+        // Since no middleware exists in this framework, register a
+        // pre-shutdown hook
+        $self = $this;
+        Signal::connect('session.close', function($null, $info) use ($self) {
+            // Whether or not the session data should be re-encoded to
+            // reflect changes made in this routine
+            $info['touched'] = $this->added_new || ($this->used && count($this->list));
+            $self->update();
+        });
+    }
+
+    function get() {
+        return $this->list;
+    }
+
+    function store($messages) {
+        $_SESSION[':msgs'] = $messages;
+        return array();
+    }
+}
diff --git a/include/class.orm.php b/include/class.orm.php
index adbf48295..124b56be7 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -920,7 +920,6 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
     var $compiler = 'MySqlCompiler';
     var $iterator = 'ModelInstanceManager';
 
-    var $params;
     var $query;
     var $count;
 
@@ -1076,8 +1075,8 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         if (isset($this->_iterator)) {
             return $this->_iterator->count();
         }
-        elseif (isset($this->_count)) {
-            return $this->_count;
+        elseif (isset($this->count)) {
+            return $this->count;
         }
         $class = $this->compiler;
         $compiler = new $class();
@@ -1167,6 +1166,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
     function __clone() {
         unset($this->_iterator);
         unset($this->query);
+        unset($this->count);
     }
 
     // IteratorAggregate interface
@@ -1250,6 +1250,7 @@ EOF;
         unset($info['limit']);
         unset($info['offset']);
         unset($info['_iterator']);
+        unset($info['count']);
         return serialize($info);
     }
 
@@ -2764,7 +2765,6 @@ class Q implements Serializable {
     const ANY =     0x0002;
 
     var $constraints;
-    var $flags;
     var $negated = false;
     var $ored = false;
 
@@ -2816,19 +2816,11 @@ class Q implements Serializable {
     }
 
     function serialize() {
-        return serialize(array(
-            'f' =>
-                ($this->negated ? self::NEGATED : 0)
-              | ($this->ored ? self::ANY : 0),
-            'c' => $this->constraints
-        ));
+        return serialize(array($this->negated, $this->ored, $this->constraints));
     }
 
     function unserialize($data) {
-        $data = unserialize($data);
-        $this->constraints = $data['c'];
-        $this->ored = $data['f'] & self::ANY;
-        $this->negated = $data['f'] & self::NEGATED;
+        list($this->negated, $this->ored, $this->constraints) = unserialize($data);
     }
 }
 ?>
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 35903aac9..23e363739 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -21,6 +21,7 @@
 require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class.
 require_once(INCLUDE_DIR.'class.migrater.php');
 require_once(INCLUDE_DIR.'class.plugin.php');
+require_once INCLUDE_DIR . 'class.message.php';
 
 define('LOG_WARN',LOG_WARNING);
 
diff --git a/include/class.ostsession.php b/include/class.ostsession.php
index f64995549..e08ab604f 100644
--- a/include/class.ostsession.php
+++ b/include/class.ostsession.php
@@ -151,8 +151,15 @@ abstract class SessionBackend {
         return $this->ttl;
     }
 
+    function write($id, $data) {
+        // Last chance session update
+        $i = new ArrayObject(array('touched' => false));
+        Signal::send('session.close', null, $i);
+        return $this->update($id, $i['touched'] ? session_encode() : $data);
+    }
+
     abstract function read($id);
-    abstract function write($id, $data);
+    abstract function update($id, $data);
     abstract function destroy($id);
     abstract function gc($maxlife);
 }
@@ -179,7 +186,7 @@ extends SessionBackend {
         return $this->data;
     }
 
-    function write($id, $data){
+    function update($id, $data){
         global $thisstaff;
 
         if (md5($id.$data) == $this->data_hash)
@@ -272,7 +279,7 @@ extends SessionBackend {
         return $data;
     }
 
-    function write($id, $data) {
+    function update($id, $data) {
         if (defined('DISABLE_SESSION') && $this->isnew)
             return;
 
diff --git a/include/class.util.php b/include/class.util.php
index 8fb9b3967..8cdec42ed 100644
--- a/include/class.util.php
+++ b/include/class.util.php
@@ -15,7 +15,9 @@ class ListObject implements IteratorAggregate, ArrayAccess, Serializable, Counta
 
     protected $storage = array();
 
-    function __construct(array $array=array()) {
+    function __construct($array=array()) {
+        if (!is_array($array) && !$array instanceof Traversable)
+            throw new InvalidArgumentException('Traversable object or array expected');
         foreach ($array as $v)
             $this->storage[] = $v;
     }
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index 0b595def7..4446de392 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -100,4 +100,8 @@ if ($lang) {
             <div id="msg_notice"><?php echo $msg; ?></div>
         <?php }elseif($warn) { ?>
             <div id="msg_warning"><?php echo $warn; ?></div>
-        <?php } ?>
+        <?php }
+        foreach (Messages::getMessages() as $M) { ?>
+            <div class="<?php echo strtolower($M->getLevel()); ?>-banner"><?php
+                echo (string) $M; ?></div>
+<?php   } ?>
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 3b9643734..48fe1f5bf 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -106,6 +106,7 @@ a time.relative {
 .info-banner { margin: 0; padding: 5px; margin-bottom: 10px; color: #3a87ad; border: 1px solid #bce8f1;  background-color: #d9edf7; }
 
 #msg_notice,
+.success-banner,
 .notice-banner { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; }
 
 #msg_warning,
diff --git a/scp/lists.php b/scp/lists.php
index 03a6daaae..35fcef37d 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -97,8 +97,7 @@ if($_POST) {
         case 'add':
             if ($list=DynamicList::add($_POST, $errors)) {
                  $form = $list->getForm(true);
-                 $msg = sprintf(__('Successfully added %s'),
-                    __('this custom list'));
+                 Messages::success(sprintf(__('Successfully added %s'), __('this custom list')));
                  // Redirect to list page
                  $redirect = "lists.php?id={$list->id}#items";
             } elseif ($errors) {
-- 
GitLab