From 52813045a62d851ba21159deb38024678438ebbe Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 3 Mar 2015 15:10:10 -0500
Subject: [PATCH 01/34] Additional parameters in provisioning when creating
 stations from dashboard

---
 .../application/common/ProvisioningHelper.php | 24 ++++++++++++++-----
 airtime_mvc/application/models/Preference.php |  5 ++++
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index 48bbddc8c..d8183a57c 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -10,6 +10,7 @@ class ProvisioningHelper
     // Parameter values
     private $dbuser, $dbpass, $dbname, $dbhost, $dbowner, $apikey;
     private $instanceId;
+    private $station_name, $description;
 
     public function __construct($apikey)
     {
@@ -34,7 +35,7 @@ class ProvisioningHelper
         try {
 
             $this->parsePostParams();
-            
+
             //For security, the Airtime Pro provisioning system creates the database for the user.
             $this->setNewDatabaseConnection();
 
@@ -58,6 +59,7 @@ class ProvisioningHelper
 
             $this->createDatabaseTables();
             $this->initializeMusicDirsTable($this->instanceId);
+            $this->initializePrefs();
         } catch (Exception $e) {
             http_response_code(400);
             Logging::error($e->getMessage()
@@ -102,6 +104,9 @@ class ProvisioningHelper
         $this->dbhost = $_POST['dbhost'];
         $this->dbowner = $_POST['dbowner'];
         $this->instanceId = $_POST['instanceid'];
+
+        $this->station_name = $_POST['station_name'];
+        $this->description = $_POST['description'];
     }
 
     /**
@@ -111,9 +116,9 @@ class ProvisioningHelper
     private function setNewDatabaseConnection()
     {
         self::$dbh = new PDO("pgsql:host=" . $this->dbhost
-            . ";dbname=" . $this->dbname
-            . ";port=5432" . ";user=" . $this->dbuser
-            . ";password=" . $this->dbpass);
+                             . ";dbname=" . $this->dbname
+                             . ";port=5432" . ";user=" . $this->dbuser
+                             . ";password=" . $this->dbpass);
         //Turn on PDO exceptions because they're off by default.
         //self::$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
         $err = self::$dbh->errorInfo();
@@ -130,8 +135,8 @@ class ProvisioningHelper
     {
         Logging::info("Creating database...");
         $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string($this->dbname)
-            . " WITH ENCODING 'UTF8' TEMPLATE template0"
-            . " OWNER " . pg_escape_string($this->dbowner));
+                                         . " WITH ENCODING 'UTF8' TEMPLATE template0"
+                                         . " OWNER " . pg_escape_string($this->dbowner));
         if (!$statement->execute()) {
             throw new Exception("ERROR: Failed to create Airtime database");
         }
@@ -182,5 +187,12 @@ class ProvisioningHelper
         $musicDir->save();
     }
 
+    /**
+     * Initialize preference values passed from the dashboard (if any exist)
+     */
+    private function initializePrefs() {
+        Application_Model_Preference::SetStationName($this->station_name);
+        Application_Model_Preference::SetStationDescription($this->description);
+    }
 
 }
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index aef7f0bb5..a25679826 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -326,6 +326,11 @@ class Application_Model_Preference
         return self::getValue("station_name");
     }
 
+    public static function SetStationName($station_name)
+    {
+        self::setValue("station_name", $station_name);
+    }
+
     public static function SetAutoUploadRecordedShowToSoundcloud($upload)
     {
         self::setValue("soundcloud_auto_upload_recorded_show", $upload);

From 95db8533b534d22fd99f68eacef6015c8c079862 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 3 Mar 2015 15:29:34 -0500
Subject: [PATCH 02/34] Made provisioning helper slightly more robust

---
 airtime_mvc/application/Bootstrap.php         |  2 +-
 .../application/common/ProvisioningHelper.php | 29 ++++++++++---------
 2 files changed, 17 insertions(+), 14 deletions(-)

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 153ce672d..7c9cf8c20 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -36,7 +36,7 @@ require_once (APPLICATION_PATH."/logging/Logging.php");
 Logging::setLogPath('/var/log/airtime/zendphp.log');
 
 // We need to manually route because we can't load Zend without the database being initialized first.
-if (array_key_exists("REQUEST_URI", $_SERVER) && (strpos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) {
+if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) {
     $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]);
     $provisioningHelper->createAction();
     die();
diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index d8183a57c..a436e6974 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -37,28 +37,31 @@ class ProvisioningHelper
             $this->parsePostParams();
 
             //For security, the Airtime Pro provisioning system creates the database for the user.
-            $this->setNewDatabaseConnection();
+            if ($this->dbhost && !empty($this->dbhost)) {
+                $this->setNewDatabaseConnection();
 
-            //if ($this->checkDatabaseExists()) {
-            //    throw new Exception("ERROR: Airtime database already exists");
-            //}
+                //if ($this->checkDatabaseExists()) {
+                //    throw new Exception("ERROR: Airtime database already exists");
+                //}
 
-            if (!$this->checkDatabaseExists()) {
-                throw new Exception("ERROR: $this->dbname database does not exist.");
-            }
+                if (!$this->checkDatabaseExists()) {
+                    throw new Exception("ERROR: $this->dbname database does not exist.");
+                }
 
-            //We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS".
-            //If we don't check, then a second call to this API endpoint would wipe all the tables!
-            if ($this->checkTablesExist()) {
-                throw new Exception("ERROR: airtime tables already exists");
+                //We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS".
+                //If we don't check, then a second call to this API endpoint would wipe all the tables!
+                if ($this->checkTablesExist()) {
+                    throw new Exception("ERROR: airtime tables already exists");
+                }
+
+                $this->createDatabaseTables();
+                $this->initializeMusicDirsTable($this->instanceId);
             }
 
             //$this->createDatabase();
 
             //All we need to do is create the database tables.
 
-            $this->createDatabaseTables();
-            $this->initializeMusicDirsTable($this->instanceId);
             $this->initializePrefs();
         } catch (Exception $e) {
             http_response_code(400);

From e2054c13c8e45b21dd71f7d6e2861e476a37e645 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 3 Mar 2015 16:04:45 -0500
Subject: [PATCH 03/34] Added provisioning code for change requests

---
 airtime_mvc/application/Bootstrap.php         |  5 ++++
 .../application/common/ProvisioningHelper.php | 29 +++++++++++++++++--
 2 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 7c9cf8c20..2db23c8f7 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -36,10 +36,15 @@ require_once (APPLICATION_PATH."/logging/Logging.php");
 Logging::setLogPath('/var/log/airtime/zendphp.log');
 
 // We need to manually route because we can't load Zend without the database being initialized first.
+// We should probably look for a better way to do this rather tan overloading this if statement
 if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) {
     $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]);
     $provisioningHelper->createAction();
     die();
+} else if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/change") !== false)) {
+    $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]);
+    $provisioningHelper->changeAction();
+    die();
 }
 
 Config::setAirtimeVersion();
diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index a436e6974..61127463a 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -65,8 +65,33 @@ class ProvisioningHelper
             $this->initializePrefs();
         } catch (Exception $e) {
             http_response_code(400);
-            Logging::error($e->getMessage()
-            );
+            Logging::error($e->getMessage());
+            echo $e->getMessage() . PHP_EOL;
+            return;
+        }
+
+        http_response_code(201);
+    }
+
+    /**
+     * Endpoint to change Airtime preferences remotely.
+     * Mainly for use with the dashboard right now.
+     */
+    public function changeAction() {
+        $apikey = $_SERVER['PHP_AUTH_USER'];
+        if (!isset($apikey) || $apikey != $this->apikey) {
+            Logging::info("Invalid API Key: $apikey");
+            http_response_code(403);
+            echo "ERROR: Incorrect API key";
+            return;
+        }
+
+        try {
+            $this->parsePostParams();
+            $this->initializePrefs();
+        } catch (Exception $e) {
+            http_response_code(400);
+            Logging::error($e->getMessage());
             echo $e->getMessage() . PHP_EOL;
             return;
         }

From 918631d676fbf09113142affdbe536bf6482fdba Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 5 Mar 2015 12:24:02 -0500
Subject: [PATCH 04/34] When calling /change, don't set prefs if empty/no
 parameters are given

---
 airtime_mvc/application/common/ProvisioningHelper.php | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index 61127463a..f8e90b11c 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -219,8 +219,12 @@ class ProvisioningHelper
      * Initialize preference values passed from the dashboard (if any exist)
      */
     private function initializePrefs() {
-        Application_Model_Preference::SetStationName($this->station_name);
-        Application_Model_Preference::SetStationDescription($this->description);
+        if ($this->statio_name) {
+            Application_Model_Preference::SetStationName($this->station_name);
+        }
+        if ($this->descption) {
+            Application_Model_Preference::SetStationDescription($this->description);
+        }
     }
 
 }

From 716244011a284909aca824bf86d7f02087409d41 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 10 Mar 2015 13:20:11 -0400
Subject: [PATCH 05/34] Removed unnecessary conditional around
 /provisioning/change in Bootstrap

---
 airtime_mvc/application/Bootstrap.php | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 2db23c8f7..7c9cf8c20 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -36,15 +36,10 @@ require_once (APPLICATION_PATH."/logging/Logging.php");
 Logging::setLogPath('/var/log/airtime/zendphp.log');
 
 // We need to manually route because we can't load Zend without the database being initialized first.
-// We should probably look for a better way to do this rather tan overloading this if statement
 if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) {
     $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]);
     $provisioningHelper->createAction();
     die();
-} else if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/change") !== false)) {
-    $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]);
-    $provisioningHelper->changeAction();
-    die();
 }
 
 Config::setAirtimeVersion();

From bd72252e9e01ab98b40be621cfd6e80a092d6f6d Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 19 Mar 2015 12:07:02 -0400
Subject: [PATCH 06/34] Fixed typos

---
 airtime_mvc/application/common/ProvisioningHelper.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index f8e90b11c..a0b44392c 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -219,10 +219,10 @@ class ProvisioningHelper
      * Initialize preference values passed from the dashboard (if any exist)
      */
     private function initializePrefs() {
-        if ($this->statio_name) {
+        if ($this->station_name) {
             Application_Model_Preference::SetStationName($this->station_name);
         }
-        if ($this->descption) {
+        if ($this->description) {
             Application_Model_Preference::SetStationDescription($this->description);
         }
     }

From 2ec2403ccec10d8e1684f766c66322061d6937e3 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 19 Mar 2015 17:32:02 -0400
Subject: [PATCH 07/34] Moved provisioning change function into controller from
 helper

---
 .../application/common/ProvisioningHelper.php | 27 +--------------
 .../controllers/ProvisioningController.php    | 34 +++++++++++++++++++
 2 files changed, 35 insertions(+), 26 deletions(-)

diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php
index a0b44392c..580c20e52 100644
--- a/airtime_mvc/application/common/ProvisioningHelper.php
+++ b/airtime_mvc/application/common/ProvisioningHelper.php
@@ -73,32 +73,6 @@ class ProvisioningHelper
         http_response_code(201);
     }
 
-    /**
-     * Endpoint to change Airtime preferences remotely.
-     * Mainly for use with the dashboard right now.
-     */
-    public function changeAction() {
-        $apikey = $_SERVER['PHP_AUTH_USER'];
-        if (!isset($apikey) || $apikey != $this->apikey) {
-            Logging::info("Invalid API Key: $apikey");
-            http_response_code(403);
-            echo "ERROR: Incorrect API key";
-            return;
-        }
-
-        try {
-            $this->parsePostParams();
-            $this->initializePrefs();
-        } catch (Exception $e) {
-            http_response_code(400);
-            Logging::error($e->getMessage());
-            echo $e->getMessage() . PHP_EOL;
-            return;
-        }
-
-        http_response_code(201);
-    }
-
     /**
      * Check if the database settings and credentials given are valid
      * @return boolean true if the database given exists and the user is valid and can access it
@@ -124,6 +98,7 @@ class ProvisioningHelper
         // Result is either boolean FALSE (no table found) or PDOStatement Object (table found)
         return $result !== FALSE;
     }
+
     private function parsePostParams()
     {
         $this->dbuser = $_POST['dbuser'];
diff --git a/airtime_mvc/application/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php
index fc0c28cbb..bc813ab0d 100644
--- a/airtime_mvc/application/controllers/ProvisioningController.php
+++ b/airtime_mvc/application/controllers/ProvisioningController.php
@@ -18,6 +18,40 @@ class ProvisioningController extends Zend_Controller_Action
      *
      */
 
+    /**
+     * Endpoint to change Airtime preferences remotely.
+     * Mainly for use with the dashboard right now.
+     */
+    public function changeAction() {
+        $this->view->layout()->disableLayout();
+        $this->_helper->viewRenderer->setNoRender(true);
+
+        if (!RestAuth::verifyAuth(true, true, $this)) {
+            return;
+        }
+
+        try {
+            // This is hacky and should be genericized
+            if ($_POST['station_name']) {
+                Application_Model_Preference::SetStationName($_POST['station_name']);
+            }
+            if ($_POST['station_name']) {
+                Application_Model_Preference::SetStationDescription($_POST['station_name']);
+            }
+        } catch (Exception $e) {
+            $this->getResponse()
+                ->setHttpResponseCode(400)
+                ->appendBody("ERROR: " . $e->getMessage());
+            Logging::error($e->getMessage());
+            echo $e->getMessage() . PHP_EOL;
+            return;
+        }
+
+        $this->getResponse()
+            ->setHttpResponseCode(200)
+            ->appendBody("OK");
+    }
+
     /**
      * Delete the Airtime Pro station's files from Amazon S3
      */

From 59206bc73b2d33b0b1edabb718b73f71ec87fbcc Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 19 Mar 2015 17:53:53 -0400
Subject: [PATCH 08/34] Fixed typo

---
 .../application/controllers/ProvisioningController.php        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php
index bc813ab0d..0ecd185d2 100644
--- a/airtime_mvc/application/controllers/ProvisioningController.php
+++ b/airtime_mvc/application/controllers/ProvisioningController.php
@@ -35,8 +35,8 @@ class ProvisioningController extends Zend_Controller_Action
             if ($_POST['station_name']) {
                 Application_Model_Preference::SetStationName($_POST['station_name']);
             }
-            if ($_POST['station_name']) {
-                Application_Model_Preference::SetStationDescription($_POST['station_name']);
+            if ($_POST['description']) {
+                Application_Model_Preference::SetStationDescription($_POST['description']);
             }
         } catch (Exception $e) {
             $this->getResponse()

From 68c4cdcab3caf0cbe351ede35413a64e0f67cae7 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Fri, 20 Mar 2015 12:36:28 -0400
Subject: [PATCH 09/34] Added missing downgrade() function to the 2.5.11
 upgrader

---
 airtime_mvc/application/upgrade/Upgrades.php | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 31c602437..4535fe475 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -381,4 +381,7 @@ class AirtimeUpgrader2511 extends AirtimeUpgrader
             throw $e;
         }
     }
+    public function downgrade() {
+
+    }
 }

From 32aa962138fa05bee64ee34a0faeccb40d58083c Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Fri, 20 Mar 2015 17:20:34 -0400
Subject: [PATCH 10/34] Fixed error logging and refactored ErrorController to
 get invoked correctly when using API key authentication

* Along with the bugfixes, backported improved ErrorController from upstream branch,
  including style improvements.
---
 airtime_mvc/application/Bootstrap.php         |   1 +
 .../controllers/ErrorController.php           |  98 ++++++++++++++----
 .../controllers/ProvisioningController.php    |   6 +-
 .../controllers/plugins/Acl_plugin.php        |  13 ++-
 .../views/scripts/error/error-400.phtml       |  18 ++++
 .../views/scripts/error/error-403.phtml       |  18 ++++
 .../views/scripts/error/error-500.phtml       |  18 ++++
 .../views/scripts/error/error.phtml           |   3 +-
 airtime_mvc/public/css/images/maintenance.png | Bin 0 -> 14597 bytes
 airtime_mvc/public/css/styles.css             |  39 ++++---
 10 files changed, 166 insertions(+), 48 deletions(-)
 create mode 100644 airtime_mvc/application/views/scripts/error/error-400.phtml
 create mode 100644 airtime_mvc/application/views/scripts/error/error-403.phtml
 create mode 100644 airtime_mvc/application/views/scripts/error/error-500.phtml
 create mode 100644 airtime_mvc/public/css/images/maintenance.png

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index b3ba22263..57ff42b0d 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -52,6 +52,7 @@ Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance());
 
 $front = Zend_Controller_Front::getInstance();
 $front->registerPlugin(new RabbitMqPlugin());
+$front->throwExceptions(false);
 
 //localization configuration
 Application_Model_Locale::configureLocalization();
diff --git a/airtime_mvc/application/controllers/ErrorController.php b/airtime_mvc/application/controllers/ErrorController.php
index 70829db63..8a62d9ea6 100644
--- a/airtime_mvc/application/controllers/ErrorController.php
+++ b/airtime_mvc/application/controllers/ErrorController.php
@@ -1,26 +1,40 @@
 <?php
+class ErrorController extends Zend_Controller_Action {
 
-class ErrorController extends Zend_Controller_Action
-{
-
-    public function errorAction()
+    public function init()
     {
+        //The default layout includes the Dashboard header, which may contain private information.
+        //We cannot show that.
+        $this->view->layout()->disableLayout();
+        $this->setupCSS();
+
+    }
+
+    public function errorAction() {
         $errors = $this->_getParam('error_handler');
 
-        switch ($errors->type) {
-            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
-            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
-            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
+        if ($errors) {
+            // log error message and stack trace
+            Logging::error($errors->exception->getMessage());
+            Logging::error($errors->exception->getTraceAsString());
 
-                // 404 error -- controller or action not found
-                $this->getResponse()->setHttpResponseCode(404);
-                $this->view->message = _('Page not found');
-                break;
-            default:
-                // application error
-                $this->getResponse()->setHttpResponseCode(500);
-                $this->view->message = _('Application error');
-                break;
+            switch ($errors->type) {
+                case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE :
+                case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER :
+                    $this->error404Action();
+                    break;
+                case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION :
+                    $this->error400Action();
+                    break;
+                default :
+                    $this->error500Action();
+                    break;
+            }
+        } else {
+            $exceptions = $this->_getAllParams();
+            Logging::error($exceptions);
+            $this->error500Action();
+            return;
         }
 
         // Log exception, if logger available
@@ -33,11 +47,17 @@ class ErrorController extends Zend_Controller_Action
             $this->view->exception = $errors->exception;
         }
 
-        $this->view->request   = $errors->request;
+        $this->view->request = $errors->request;
     }
 
-    public function getLog()
+    private function setupCSS()
     {
+        $CC_CONFIG = Config::getConfig();
+        $staticBaseDir = Application_Common_OsPath::formatDirectoryWithDirectorySeparators($CC_CONFIG['staticBaseDir']);
+        $this->view->headLink()->appendStylesheet($staticBaseDir . 'css/styles.css?' . $CC_CONFIG['airtime_version']);
+    }
+
+    public function getLog() {
         $bootstrap = $this->getInvokeArg('bootstrap');
         if (!$bootstrap->hasPluginResource('Log')) {
             return false;
@@ -47,9 +67,43 @@ class ErrorController extends Zend_Controller_Action
         return $log;
     }
 
-    public function deniedAction()
-    {
-        // action body
+    /**
+     * 404 error - route or controller
+     */
+    public function error404Action() {
+        $this->_helper->viewRenderer('error-404');
+        $this->getResponse()->setHttpResponseCode(404);
+        $this->view->message = _('Page not found.');
     }
 
+    /**
+     * 400 error - no such action
+     */
+    public function error400Action() {
+        $this->_helper->viewRenderer('error-400');
+        $this->getResponse()->setHttpResponseCode(400);
+        $this->view->message = _('The requested action is not supported.');
+
+    }
+
+    /**
+     * 403 error - permission denied
+     */
+    public function error403Action() {
+
+        $this->_helper->viewRenderer('error-403');
+        $this->getResponse()->setHttpResponseCode(403);
+        $this->view->message = _('You do not have permission to access this resource.');
+    }
+
+    /**
+     * 500 error - internal server error
+     */
+    public function error500Action() {
+
+        $this->_helper->viewRenderer('error-500');
+
+        $this->getResponse()->setHttpResponseCode(500);
+        $this->view->message = _('An internal application error has occurred.');
+    }
 }
diff --git a/airtime_mvc/application/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php
index 0ecd185d2..4aa02fa39 100644
--- a/airtime_mvc/application/controllers/ProvisioningController.php
+++ b/airtime_mvc/application/controllers/ProvisioningController.php
@@ -26,7 +26,7 @@ class ProvisioningController extends Zend_Controller_Action
         $this->view->layout()->disableLayout();
         $this->_helper->viewRenderer->setNoRender(true);
 
-        if (!RestAuth::verifyAuth(true, true, $this)) {
+        if (!RestAuth::verifyAuth(true, false, $this)) {
             return;
         }
 
@@ -65,12 +65,12 @@ class ProvisioningController extends Zend_Controller_Action
         }
         
         $CC_CONFIG = Config::getConfig();
-        
+
         foreach ($CC_CONFIG["supportedStorageBackends"] as $storageBackend) {
             $proxyStorageBackend = new ProxyStorageBackend($storageBackend);
             $proxyStorageBackend->deleteAllCloudFileObjects();
         }
-        
+
         $this->getResponse()
             ->setHttpResponseCode(200)
             ->appendBody("OK");
diff --git a/airtime_mvc/application/controllers/plugins/Acl_plugin.php b/airtime_mvc/application/controllers/plugins/Acl_plugin.php
index 10910fb73..7ea1336d0 100644
--- a/airtime_mvc/application/controllers/plugins/Acl_plugin.php
+++ b/airtime_mvc/application/controllers/plugins/Acl_plugin.php
@@ -28,7 +28,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
     {
         $this->_errorPage = array('module' => 'default',
                                   'controller' => 'error',
-                                  'action' => 'denied');
+                                  'action' => 'error');
 
         $this->_roleName = $roleName;
 
@@ -111,7 +111,16 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
         $controller = strtolower($request->getControllerName());
         Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance());
 
-        if (in_array($controller, array("api", "auth", "locale", "upgrade", 'whmcs-login', "provisioning"))) {
+        if (in_array($controller, array(
+                "api",
+                "auth",
+                "error",
+                "locale",
+                "upgrade",
+                'whmcs-login',
+                "provisioning"
+            )))
+        {
             $this->setRoleName("G");
         } elseif (!Zend_Auth::getInstance()->hasIdentity()) {
 
diff --git a/airtime_mvc/application/views/scripts/error/error-400.phtml b/airtime_mvc/application/views/scripts/error/error-400.phtml
new file mode 100644
index 000000000..329b55228
--- /dev/null
+++ b/airtime_mvc/application/views/scripts/error/error-400.phtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN";
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title><?php echo _("An error has occurred.") ?></title>
+  <?php echo $this->headLink(); ?>
+</head>
+<body>
+    <div class="error-content" id="error-400">
+        <h2><?php echo _("Bad Request!")?></h2>
+        <p><?php echo _("The requested action is not supported!")?></p>
+        <div class="button-bar">
+            <a class="toggle-button" href="<?php echo $this->baseUrl('dashboard/help'); ?>"><?php echo _("Help") ?></a>
+        </div>
+    </div>
+</body>
+</html>
diff --git a/airtime_mvc/application/views/scripts/error/error-403.phtml b/airtime_mvc/application/views/scripts/error/error-403.phtml
new file mode 100644
index 000000000..0e8d781fd
--- /dev/null
+++ b/airtime_mvc/application/views/scripts/error/error-403.phtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN";
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title><?php echo _("An error has occurred.") ?></title>
+  <?php echo $this->headLink(); ?>
+</head>
+<body>
+    <div class="error-content" id="error-403">
+        <h2><?php echo _("Access Denied!")?></h2>
+        <p><?php echo _("You do not have permission to access this page!")?></p>
+        <div class="button-bar">
+            <a class="toggle-button" href="<?php echo $this->baseUrl('dashboard/help'); ?>"><?php echo _("Help") ?></a>
+        </div>
+    </div>
+</body>
+</html>
diff --git a/airtime_mvc/application/views/scripts/error/error-500.phtml b/airtime_mvc/application/views/scripts/error/error-500.phtml
new file mode 100644
index 000000000..fe5bd9f39
--- /dev/null
+++ b/airtime_mvc/application/views/scripts/error/error-500.phtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN";
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title><?php echo _("An error has occurred.") ?></title>
+  <?php echo $this->headLink(); ?>
+</head>
+<body>
+    <div class="error-content" id="error-500">
+        <h2><?php echo _("Oops!")?></h2>
+        <p><?php echo _("Something went wrong!")?></p>
+        <div class="button-bar">
+            <a class="toggle-button" href="<?php echo $this->baseUrl('dashboard/help'); ?>"><?php echo _("Help") ?></a>
+        </div>
+    </div>
+</body>
+</html>
diff --git a/airtime_mvc/application/views/scripts/error/error.phtml b/airtime_mvc/application/views/scripts/error/error.phtml
index 4c5146296..7a17ae7c6 100644
--- a/airtime_mvc/application/views/scripts/error/error.phtml
+++ b/airtime_mvc/application/views/scripts/error/error.phtml
@@ -3,7 +3,8 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-  <title><?php echo _("Zend Framework Default Application") ?></title>
+  <title><?php echo _("An error has occurred.") ?></title>
+  <?php echo $this->headLink(); ?>
 </head>
 <body>
     <div class="error-content">
diff --git a/airtime_mvc/public/css/images/maintenance.png b/airtime_mvc/public/css/images/maintenance.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a000c6c6a68df65086a5f99fcaa471195c197f1
GIT binary patch
literal 14597
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

literal 0
HcmV?d00001

diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css
index 31a3f8383..d709bf038 100644
--- a/airtime_mvc/public/css/styles.css
+++ b/airtime_mvc/public/css/styles.css
@@ -899,20 +899,19 @@ input[type="checkbox"] {
 
 /* Remove any visible csrf form token footprint */
 #csrf-label {
-    height: 0;
-    padding: 0;
-    margin: 0;
+    display: none;
 }
 
 #csrf-element {
-    height: 8px;
     padding: 0;
     margin: 0;
+    display: inline-block;
 }
 
+/*
 #csrf-label .errors li, #csrf-element .errors li {
     margin: 0;
-}
+}*/
 
 .login_box {
     margin: 0 auto 0 auto;
@@ -1031,7 +1030,6 @@ input[type="checkbox"] {
 #pref_form p.description {
     color: #3b3b3b;
     font-size: 12px;
-    float: left;
 }
 
   dt.block-display, dd.block-display {
@@ -2193,7 +2191,7 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio
     width: 98.5%;
 }
 
-.preferences dd#SoundCloudTags-element.block-display .input_text_area {
+.preferences dd.block-display .input_text_area {
     height: 120px;
 }
 
@@ -2202,14 +2200,10 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio
 }
 
 .preferences #logo-remove-btn {
-    float: right;
+    /*float: left;*/
     margin-bottom: 4px;
 }
 
-.preferences #Logo-img-container {
-    margin-top: 30px;
-}
-
 #show_time_info {
     font-size:12px;
     height:30px;
@@ -2570,19 +2564,21 @@ dt.block-display.info-block {
 
 /*---////////////////////    ERROR PAGE    ////////////////////---*/
 
-.error-content    {
-    background:url(images/404.png) no-repeat 0 0;
-    width:300px;
-    margin: 24px 15px;
-    padding: 0px 10px 0 420px;
+.error-content {
+    background:url(images/maintenance.png) no-repeat 0 0;
+    width:360px;
+    height:350px;
+    margin:auto;
+    margin-top:25px;
+    padding:auto;
 }
 .error-content h2 {
     margin:0;
-    padding:0 0 10px 0;
+    padding:350px 0 10px 0;
     font-size:36px;
     font-weight:bold;
     color:#3e3e3e;
-    text-align:left;
+    text-align:center;
     letter-spacing:-.3px;
     text-shadow: rgba(248,248,248,.3) 0 1px 0, rgba(0,0,0,.8) 0 -1px 0;
     rgba(51,51,51,.9)
@@ -2590,12 +2586,14 @@ dt.block-display.info-block {
 .error-content p {
     color: #272727;
     font-size: 16px;
+    text-align:center;
     margin: 0;
     padding:8px 2px;
 }
-.error-content .button-bar    {
+.error-content .button-bar	{
     margin-top:47px;
     padding-left:2px;
+    text-align:center;
 }
 .error-content .toggle-button    {
     border: 1px solid #434343;
@@ -3142,3 +3140,4 @@ dd .stream-status {
 }
 .quota-reached {
     font-size: 14px !important;
+}

From 8c7ae6c890a2bf6b96e7011816963fe10cb38259 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Fri, 20 Mar 2015 17:23:53 -0400
Subject: [PATCH 11/34] Added safety checks to Amazon_S3StorageBackend

---
 .../cloud_storage/Amazon_S3StorageBackend.php | 26 +++++++++++++++----
 1 file changed, 21 insertions(+), 5 deletions(-)

diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
index e2b6a1ba6..6a05bd9a5 100644
--- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
@@ -53,14 +53,30 @@ class Amazon_S3StorageBackend extends StorageBackend
     // Records in the database will remain in case we have to restore the files.
     public function deleteAllCloudFileObjects()
     {
-        $this->s3Client->deleteMatchingObjects(
-            $bucket = $this->getBucket(),
-            $prefix = $this->getFilePrefix());
+        $bucket = $this->getBucket();
+        $prefix = $this->getFilePrefix();
+
+        //Add a trailing slash in for safety
+        //(so that deleting /13/413 doesn't delete /13/41313 !)
+        $prefix = $prefix . "/";
+
+        //Do a bunch of safety checks to ensure we don't delete more than we intended.
+        //An valid prefix is like "12/4312" for instance 4312.
+        $slashPos = strpos($prefix, "/");
+        if (($slashPos === FALSE) || //Slash must exist
+            ($slashPos != 2) ||      //Slash must be the third character
+            (strlen($prefix) > $slashPos) ||    //String must have something after the first slash
+            (substr_count($prefix, "/") != 2))  //String must have two slashes
+        {
+            throw new Exception("Invalid file prefix in " . __FUNCTION__);
     }
-    
+        $this->s3Client->deleteMatchingObjects($bucket, $prefix);
+    }
+
     public function getFilePrefix()
     {
         $hostingId = Billing::getClientInstanceId();
-        return substr($hostingId, -2)."/".$hostingId;
+        $filePrefix = substr($hostingId, -2)."/".$hostingId;
+        return $filePrefix;
     }
 }

From 7b9efb988f2a21391dd661063e922fcdbb013056 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Fri, 20 Mar 2015 18:03:23 -0400
Subject: [PATCH 12/34] Fixed logic error Amazon_S3StorageBackend

---
 .../application/cloud_storage/Amazon_S3StorageBackend.php       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
index 6a05bd9a5..b51b0059f 100644
--- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
@@ -65,7 +65,7 @@ class Amazon_S3StorageBackend extends StorageBackend
         $slashPos = strpos($prefix, "/");
         if (($slashPos === FALSE) || //Slash must exist
             ($slashPos != 2) ||      //Slash must be the third character
-            (strlen($prefix) > $slashPos) ||    //String must have something after the first slash
+            (strlen($prefix) <= $slashPos) ||    //String must have something after the first slash
             (substr_count($prefix, "/") != 2))  //String must have two slashes
         {
             throw new Exception("Invalid file prefix in " . __FUNCTION__);

From 3d03f837d24908c0ad279bb229997f16b6c2474a Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 10:11:25 -0400
Subject: [PATCH 13/34] Trial->Paid conversion tracking with GTM

* Added trial to paid conversion tracking with GTM
* Removed WHMCS roundtrip from Showbuilder
* Moved all Analytics code into common/GoogleAnalytics.php
* Added a new Thank You page after plan changes to capture conversions
* Added a ConversionTracking plugin to facilitate that
* Also backported some minor staticBaseDir compatibility changes
* Fixed a logic error in creating the baseDir
---
 airtime_mvc/application/Bootstrap.php         |   3 +
 .../application/common/GoogleAnalytics.php    |  90 +++++++++++++++
 airtime_mvc/application/common/HTTPHelper.php |   2 +-
 airtime_mvc/application/configs/ACL.php       |   2 +
 airtime_mvc/application/configs/conf.php      |   7 ++
 .../controllers/ShowbuilderController.php     | 103 +-----------------
 .../controllers/ThankYouController.php        |  48 ++++++++
 .../plugins/ConversionTracking.php            |  21 ++++
 airtime_mvc/application/models/Preference.php |  16 +++
 .../views/scripts/thank-you/index.phtml       |  20 ++++
 airtime_mvc/public/css/styles.css             |  12 ++
 11 files changed, 222 insertions(+), 102 deletions(-)
 create mode 100644 airtime_mvc/application/common/GoogleAnalytics.php
 create mode 100644 airtime_mvc/application/controllers/ThankYouController.php
 create mode 100644 airtime_mvc/application/controllers/plugins/ConversionTracking.php
 create mode 100644 airtime_mvc/application/views/scripts/thank-you/index.phtml

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 57ff42b0d..91736f51a 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -24,12 +24,14 @@ require_once "FileIO.php";
 require_once "OsPath.php";
 require_once "Database.php";
 require_once "ProvisioningHelper.php";
+require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
 require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
 require_once __DIR__.'/controllers/plugins/Maintenance.php';
+require_once __DIR__.'/controllers/plugins/ConversionTracking.php';
 require_once __DIR__.'/modules/rest/controllers/ShowImageController.php';
 require_once __DIR__.'/modules/rest/controllers/MediaController.php';
 
@@ -52,6 +54,7 @@ Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance());
 
 $front = Zend_Controller_Front::getInstance();
 $front->registerPlugin(new RabbitMqPlugin());
+$front->registerPlugin(new Zend_Controller_Plugin_ConversionTracking());
 $front->throwExceptions(false);
 
 //localization configuration
diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php
new file mode 100644
index 000000000..44d951cc7
--- /dev/null
+++ b/airtime_mvc/application/common/GoogleAnalytics.php
@@ -0,0 +1,90 @@
+<?php
+
+class Application_Common_GoogleAnalytics
+{
+
+    /** Returns a string containing the JavaScript code to pass some billing account info
+     *  into Google Tag Manager / Google Analytics, so we can track things like the plan type.
+     */
+    public static function generateGoogleTagManagerDataLayerJavaScript()
+    {
+        $code = "";
+
+        try {
+            $clientId = Application_Model_Preference::GetClientId();
+
+            $plan = Application_Model_Preference::GetPlanLevel();
+            $isTrial = ($plan == "trial");
+
+            //Figure out how long the customer has been around using a mega hack.
+            //(I'm avoiding another round trip to WHMCS for now...)
+            //We calculate it based on the trial end date...
+            $trialEndDateStr = Application_Model_Preference::GetTrialEndingDate();
+            if ($trialEndDateStr == '') {
+                $accountDuration = 0;
+            } else {
+                $today = new DateTime();
+                $trialEndDate = new DateTime($trialEndDateStr);
+                $trialDuration = new DateInterval("P30D"); //30 day trial duration
+                $accountCreationDate = $trialEndDate->sub($trialDuration);
+                $interval = $today->diff($accountCreationDate);
+                $accountDuration = $interval->days;
+            }
+
+            $code = "$( document ).ready(function() {
+                    dataLayer.push({
+                                    'UserID':  '" . $clientId . "',
+                                    'Customer':  'Customer',
+                                    'PlanType':  '" . $plan . "',
+                                    'Trial':  '" . $isTrial . "',
+                                    'AccountDuration':  '" . strval($accountDuration) . "'
+                                    });
+                     });";
+            //No longer sending these variables because we used to make a query to WHMCS
+            //to fetch them, which was slow.
+            //               'ZipCode':  '" . $postcode . "',
+            //               'Country':  '" . $country . "',
+
+        } catch (Exception $e) {
+            Logging::error($e);
+            return "";
+        }
+        return $code;
+    }
+
+    /** Generate the JavaScript snippet that logs a trial to paid conversion  */
+    public static function generateConversionTrackingJavaScript()
+    {
+        $newPlan = Application_Model_Preference::GetPlanLevel();
+        $oldPlan = Application_Model_Preference::GetOldPlanLevel();
+
+        $code = "dataLayer.push({'event': 'Conversion',
+                                 'Conversion': 'Trial to Paid',
+                                 'Old Plan' : '$oldPlan',
+                                 'New Plan' : '$newPlan'});";
+        return $code;
+    }
+
+    /** Return true if the user used to be on a trial plan and was just converted to a paid plan. */
+    public static function didPaidConversionOccur($request)
+    {
+        $userInfo = Zend_Auth::getInstance()->getStorage()->read();
+        if ($userInfo) {
+            $user = new Application_Model_User($userInfo->id);
+        } else {
+            return;
+        }
+
+        $oldPlan = Application_Model_Preference::GetOldPlanLevel();
+
+        if ($user->isSuperAdmin() && $request->getControllerKey() !== "thank-you")
+        {
+            //Only tracking trial->paid conversions for now.
+            if ($oldPlan == "trial")
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php
index 177892e1f..d6f1d6820 100644
--- a/airtime_mvc/application/common/HTTPHelper.php
+++ b/airtime_mvc/application/common/HTTPHelper.php
@@ -27,7 +27,7 @@ class Application_Common_HTTPHelper
         if (empty($baseDir)) {
             $baseDir = "/";
         }
-        if ($baseDir[0] != "") {
+        if ($baseDir[0] != "/") {
             $baseDir = "/" . $baseDir;
         }
 
diff --git a/airtime_mvc/application/configs/ACL.php b/airtime_mvc/application/configs/ACL.php
index 8043a7e76..883656fd7 100644
--- a/airtime_mvc/application/configs/ACL.php
+++ b/airtime_mvc/application/configs/ACL.php
@@ -36,6 +36,7 @@ $ccAcl->add(new Zend_Acl_Resource('library'))
       ->add(new Zend_Acl_Resource('rest:media'))
       ->add(new Zend_Acl_Resource('rest:show-image'))
       ->add(new Zend_Acl_Resource('billing'))
+      ->add(new Zend_Acl_Resource('thank-you'))
       ->add(new Zend_Acl_Resource('provisioning'));
 
 /** Creating permissions */
@@ -69,6 +70,7 @@ $ccAcl->allow('G', 'index')
       ->allow('A', 'user')
       ->allow('A', 'systemstatus')
       ->allow('A', 'preference')
+      ->allow('S', 'thank-you')
       ->allow('S', 'billing');
       
 
diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index eae99d778..f89a5472e 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -44,6 +44,13 @@ class Config {
             $CC_CONFIG['dev_env'] = 'production';
         }
 
+        //Backported static_base_dir default value into saas for now.
+        if (array_key_exists('static_base_dir', $values['general'])) {
+            $CC_CONFIG['staticBaseDir'] = $values['general']['static_base_dir'];
+        } else {
+            $CC_CONFIG['staticBaseDir'] = '/';
+        }
+
         // Parse separate conf file for cloud storage values
         $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf";
         if (!file_exists($cloudStorageConfig)) {
diff --git a/airtime_mvc/application/controllers/ShowbuilderController.php b/airtime_mvc/application/controllers/ShowbuilderController.php
index 5638cf719..3bb30abf4 100644
--- a/airtime_mvc/application/controllers/ShowbuilderController.php
+++ b/airtime_mvc/application/controllers/ShowbuilderController.php
@@ -35,7 +35,7 @@ class ShowbuilderController extends Zend_Controller_Action
         $user = Application_Model_User::GetCurrentUser();
         $userType = $user->getType();
         $this->view->headScript()->appendScript("localStorage.setItem( 'user-type', '$userType' );");
-        $this->view->headScript()->appendScript($this->generateGoogleTagManagerDataLayerJavaScript());
+        $this->view->headScript()->appendScript(Application_Common_GoogleAnalytics::generateGoogleTagManagerDataLayerJavaScript());
 
         $this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
         $this->view->headScript()->appendFile($baseUrl.'js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
@@ -367,104 +367,5 @@ class ShowbuilderController extends Zend_Controller_Action
         throw new Exception("this controller is/was a no-op please fix your
            code");
     }
-    
-    /** Returns a string containing the JavaScript code to pass some billing account info 
-     *  into Google Tag Manager / Google Analytics, so we can track things like the plan type.
-     */
-    private static function generateGoogleTagManagerDataLayerJavaScript()
-    {
-        $code = "";
-        
-        try
-        {
-            $accessKey = $_SERVER["WHMCS_ACCESS_KEY"];
-            $username = $_SERVER["WHMCS_USERNAME"];
-            $password = $_SERVER["WHMCS_PASSWORD"];
-            $url = "https://account.sourcefabric.com/includes/api.php?accesskey=" . $accessKey; # URL to WHMCS API file goes here
-            
-            $postfields = array();
-            $postfields["username"] = $username;
-            $postfields["password"] = md5($password);
-            $postfields["action"] = "getclientsdetails";
-            $postfields["stats"] = true;
-            $postfields["clientid"] = Application_Model_Preference::GetClientId();
-            $postfields["responsetype"] = "json";
-            
-            $query_string = "";
-            foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&";
-            
-            $ch = curl_init();
-            curl_setopt($ch, CURLOPT_URL, $url);
-            curl_setopt($ch, CURLOPT_POST, 1);
-            curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); // WHMCS IP whitelist doesn't support IPv6
-            curl_setopt($ch, CURLOPT_TIMEOUT, 5); //Aggressive 5 second timeout
-            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-            curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string);
-            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
-            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
-            $jsondata = curl_exec($ch);
-            if (curl_error($ch)) {
-                //die("Connection Error: ".curl_errno($ch).' - '.curl_error($ch));
-                throw new Exception("WHMCS server down or invalid request.");
-            }
-            curl_close($ch);
-                    
-            $arr = json_decode($jsondata); # Decode JSON String
-            
-            if ($arr->result !== "success") {
-                Logging::warn("WHMCS API call failed in " . __FUNCTION__); 
-                return;
-            }
-            
-            $client = $arr->client;
-            $stats = $arr->stats;
-            $currencyCode = $client->currency_code;
-            //$incomeCents = NumberFormatter::parseCurrency($stats->income, $currencyCode);
-            
-            $isTrial = true;
-            if (strpos($stats->income, "0.00") === FALSE) {
-                $isTrial = false;
-            }
-            /*
-            if ($incomeCents > 0) {
-                $isTrial = false;
-            }*/
-            $plan = Application_Model_Preference::GetPlanLevel();
-            $country = $client->country;
-            $postcode = $client->postcode;
-            
-            //Figure out how long the customer has been around using a mega hack.
-            //(I'm avoiding another round trip to WHMCS for now...)
-            //We calculate it based on the trial end date...
-            $trialEndDateStr = Application_Model_Preference::GetTrialEndingDate();
-            if ($trialEndDateStr == '') {
-                $accountDuration = 0;
-            } else {
-                $today = new DateTime();
-                $trialEndDate = new DateTime($trialEndDateStr);
-                $trialDuration = new DateInterval("P30D"); //30 day trial duration
-                $accountCreationDate = $trialEndDate->sub($trialDuration);
-                $interval = $today->diff($accountCreationDate);
-                $accountDuration = $interval->days;
-            }
-            
-            $code = "$( document ).ready(function() {
-                    dataLayer.push({
-                                    'ZipCode':  '" . $postcode . "',
-                                    'UserID':  '" . $client->id . "',
-                                    'Customer':  'Customer',
-                                    'PlanType':  '" . $plan . "',
-                                    'Trial':  '" . $isTrial . "',
-                                    'Country':  '" . $country . "',
-                                    'AccountDuration':  '" . strval($accountDuration) . "'
-                                    });
-                     });";
-            
-        } 
-        catch (Exception $e)
-        {
-            return "";
-        }
-        return $code;
-    }
+
 }
diff --git a/airtime_mvc/application/controllers/ThankYouController.php b/airtime_mvc/application/controllers/ThankYouController.php
new file mode 100644
index 000000000..86a57a54c
--- /dev/null
+++ b/airtime_mvc/application/controllers/ThankYouController.php
@@ -0,0 +1,48 @@
+<?php
+
+class ThankYouController extends Zend_Controller_Action
+{
+    public function indexAction()
+    {
+        //Variable for the template (thank-you/index.phtml)
+        $this->view->stationUrl = Application_Common_HTTPHelper::getStationUrl();
+        $this->view->conversionUrl  = Application_Common_HTTPHelper::getStationUrl() . 'thank-you/confirm-conversion';
+        $this->view->gaEventTrackingJsCode = ""; //Google Analytics event tracking code that logs an event.
+
+        // Embed the Google Analytics conversion tracking code if the
+        // user is a super admin and old plan level is set to trial.
+        if (Application_Common_GoogleAnalytics::didPaidConversionOccur($this->getRequest())) {
+            $this->view->gaEventTrackingJsCode = Application_Common_GoogleAnalytics::generateConversionTrackingJavaScript();
+        }
+
+        $csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
+        $csrf_element = new Zend_Form_Element_Hidden('csrf');
+        $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label');
+        $csrf_form = new Zend_Form();
+        $csrf_form->addElement($csrf_element);
+        $this->view->form = $csrf_form;
+    }
+
+    /** Confirm that a conversion was tracked. */
+    public function confirmConversionAction()
+    {
+        $this->view->layout()->disableLayout();
+        $this->_helper->viewRenderer->setNoRender(true);
+
+        $current_namespace = new Zend_Session_Namespace('csrf_namespace');
+        $observed_csrf_token = $this->_getParam('csrf_token');
+        $expected_csrf_token = $current_namespace->authtoken;
+
+        if($observed_csrf_token != $expected_csrf_token) {
+            Logging::info("Invalid CSRF token");
+            return;
+        }
+
+        if ($this->getRequest()->isPost()) {
+            Logging::info("Goal conversion from trial to paid.");
+            // Clear old plan level so we prevent duplicate events.
+            // This should only be called from AJAX. See thank-you/index.phtml
+            Application_Model_Preference::ClearOldPlanLevel();
+        }
+    }
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/controllers/plugins/ConversionTracking.php b/airtime_mvc/application/controllers/plugins/ConversionTracking.php
new file mode 100644
index 000000000..09904828d
--- /dev/null
+++ b/airtime_mvc/application/controllers/plugins/ConversionTracking.php
@@ -0,0 +1,21 @@
+<?php
+
+class Zend_Controller_Plugin_ConversionTracking extends Zend_Controller_Plugin_Abstract
+{
+    public function preDispatch(Zend_Controller_Request_Abstract $request)
+    {
+        //If user is a super admin and old plan level is set to trial....
+        if (Application_Common_GoogleAnalytics::didPaidConversionOccur($request))
+        {
+            //Redirect to Thank you page, unless the request was already going there...
+            if ($request->getControllerName() != 'thank-you')
+            {
+                $request->setModuleName('default')
+                    ->setControllerName('thank-you')
+                    ->setActionName('index')
+                    ->setDispatched(true);
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index a25679826..b7ea8a70e 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -824,7 +824,10 @@ class Application_Model_Preference
 
     public static function SetPlanLevel($plan)
     {
+        $oldPlanLevel = GetPlanLevel();
         self::setValue("plan_level", $plan);
+        //We save the old plan level temporarily to facilitate conversion tracking
+        self::setValue("old_plan_level", $oldPlanLevel);
     }
 
     public static function GetPlanLevel()
@@ -837,6 +840,19 @@ class Application_Model_Preference
         return $plan;
     }
 
+    public static function GetOldPlanLevel()
+    {
+        $oldPlan = self::getValue("old_plan_level");
+        return $oldPlan;
+    }
+
+    /** Clearing the old plan level indicates a change in your plan has been tracked (Google Analytics) */
+    public static function ClearOldPlanLevel()
+    {
+        self::setValue("old_plan_level", '');
+    }
+
+
     public static function SetTrialEndingDate($date)
     {
         self::setValue("trial_end_date", $date);
diff --git a/airtime_mvc/application/views/scripts/thank-you/index.phtml b/airtime_mvc/application/views/scripts/thank-you/index.phtml
new file mode 100644
index 000000000..436b9b378
--- /dev/null
+++ b/airtime_mvc/application/views/scripts/thank-you/index.phtml
@@ -0,0 +1,20 @@
+<script type="text/javascript">
+    $(document).ready(function() {
+        <?php if ($this->gaEventTrackingJsCode != "") {
+            echo($this->gaEventTrackingJsCode);
+        ?>
+        jQuery.post("<?=$this->conversionUrl?>", { "csrf_token" : $("#csrf").attr('value')},
+            function( data ) {});
+        <?php }; //endif ?>
+    });
+</script>
+<?php echo $this->form->getElement('csrf') ?>
+
+<div class="ui-widget ui-widget-content block-shadow clearfix padded-strong thankyou-panel">
+    <center>
+        <div class="logobox" style="margin-left: 32px;"></div>
+        <h2><?php echo _pro("Thank you!")?></h2>
+        <h3><?php echo _pro("Your station has been upgraded successfully.")?></h3>
+        <p><a href="<?=$this->stationUrl?>"><?php echo _pro("Return to Airtime")?></a></p>
+    </center>
+</div>
diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css
index d709bf038..67eb50aaf 100644
--- a/airtime_mvc/public/css/styles.css
+++ b/airtime_mvc/public/css/styles.css
@@ -3141,3 +3141,15 @@ dd .stream-status {
 .quota-reached {
     font-size: 14px !important;
 }
+
+.thankyou-panel
+{
+    width: 400px;
+    margin: 0 auto;
+    margin-bottom: 30px;
+}
+
+.thankyou-panel h3
+{
+    color: #222;
+}

From 084c6dd662165e21d55d68a6b388fbc5700fe660 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 10:34:48 -0400
Subject: [PATCH 14/34] PHP compatibility fix

---
 airtime_mvc/application/models/Preference.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index b7ea8a70e..bc0b2006e 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -824,7 +824,7 @@ class Application_Model_Preference
 
     public static function SetPlanLevel($plan)
     {
-        $oldPlanLevel = GetPlanLevel();
+        $oldPlanLevel = self::GetPlanLevel();
         self::setValue("plan_level", $plan);
         //We save the old plan level temporarily to facilitate conversion tracking
         self::setValue("old_plan_level", $oldPlanLevel);

From bc2acaea51e857bd048d9745474462afd3dda1d3 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 11:18:03 -0400
Subject: [PATCH 15/34] Don't fire conversion tracking for the
 sourcefabric_admin user

---
 airtime_mvc/application/common/GoogleAnalytics.php | 4 +++-
 airtime_mvc/application/models/User.php            | 9 +++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php
index 44d951cc7..00a95a51e 100644
--- a/airtime_mvc/application/common/GoogleAnalytics.php
+++ b/airtime_mvc/application/common/GoogleAnalytics.php
@@ -77,7 +77,9 @@ class Application_Common_GoogleAnalytics
 
         $oldPlan = Application_Model_Preference::GetOldPlanLevel();
 
-        if ($user->isSuperAdmin() && $request->getControllerKey() !== "thank-you")
+        if ($user->isSuperAdmin() &&
+            !$user->isSourcefabricAdmin() &&
+            $request->getControllerKey() !== "thank-you")
         {
             //Only tracking trial->paid conversions for now.
             if ($oldPlan == "trial")
diff --git a/airtime_mvc/application/models/User.php b/airtime_mvc/application/models/User.php
index 2001a97f8..ac428fefc 100644
--- a/airtime_mvc/application/models/User.php
+++ b/airtime_mvc/application/models/User.php
@@ -69,6 +69,15 @@ class Application_Model_User
         return $result;
     }
 
+    public function isSourcefabricAdmin()
+    {
+        $username = $this->getLogin();
+        if ($username == "sourcefabric_admin") {
+            return true;
+        }
+        return false;
+    }
+
     // TODO : refactor code to only accept arrays for isUserType and
     // simplify code even further
     public function isUserType($type)

From d1b28fd5643f338202bb8e59313b2397230f33ce Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 14:56:03 -0400
Subject: [PATCH 16/34] Fixed a bug in FTP upload hook, other minor
 improvements

* Ensure we have write permissions on any newly uploaded files by FTP
* Don't silence warnings in moveFileToStor()
* Reduced the FTP upload max retry count
* Log E_NOTICE and E_WARNING levels to Sentry
---
 airtime_mvc/application/logging/Logging.php                 | 2 ++
 airtime_mvc/application/models/StoredFile.php               | 3 +--
 .../modules/rest/controllers/MediaController.php            | 1 +
 python_apps/airtime_analyzer/tools/ftp-upload-hook.sh       | 6 +++++-
 4 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/airtime_mvc/application/logging/Logging.php b/airtime_mvc/application/logging/Logging.php
index 5aef4baba..d6d30e932 100644
--- a/airtime_mvc/application/logging/Logging.php
+++ b/airtime_mvc/application/logging/Logging.php
@@ -138,7 +138,9 @@ class Logging {
         switch($err['type'])
         {
             case E_ERROR:
+            case E_WARNING:
             case E_PARSE:
+            case E_NOTICE:
             case E_CORE_ERROR:
             case E_CORE_WARNING:
             case E_COMPILE_ERROR:
diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index 5e6e53161..e73d0a246 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -986,13 +986,12 @@ SQL;
 
             // Martin K.: changed to rename: Much less load + quicker since this is
             // an atomic operation
-            if (@rename($audio_file, $audio_stor) === false) {
+            if (rename($audio_file, $audio_stor) === false) {
                 //something went wrong likely there wasn't enough space in .
                 //the audio_stor to move the file too warn the user that   .
                 //the file wasn't uploaded and they should check if there  .
                 //is enough disk space                                     .
                 unlink($audio_file); //remove the file after failed rename
-                //unlink($id_file); // Also remove the identifier file
 
                 throw new Exception("The file was not uploaded, this error can occur if the computer "
                     . "hard drive does not have enough disk space or the stor "
diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php
index 6a2cccd63..b43bac3c0 100644
--- a/airtime_mvc/application/modules/rest/controllers/MediaController.php
+++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php
@@ -124,6 +124,7 @@ class Rest_MediaController extends Zend_Rest_Controller
         catch (Exception $e) {
             $this->unknownErrorResponse();
             Logging::error($e->getMessage());
+            throw $e;
         }
     }
 
diff --git a/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh b/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh
index aa543f853..4304f7594 100755
--- a/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh
+++ b/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh
@@ -2,10 +2,14 @@
 
 post_file() {
     #kill process after 30 minutes (360*5=30 minutes)
-    max_retry=360
+    max_retry=5
     retry_count=0
 
     file_path="${1}"
+    # Give us write permissions on the file to prevent problems if the user
+    # uploads a read-only file.
+    chmod +w "${file_path}"
+
     #We must remove commas because CURL can't upload files with commas in the name
     # http://curl.haxx.se/mail/archive-2009-07/0029.html
     stripped_file_path=${file_path//','/''}

From 2f4f4b05b90e1b5bf0620ecf3b227cdeba264648 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 16:54:09 -0400
Subject: [PATCH 17/34] Squash warning in php-amqplib

---
 airtime_mvc/library/php-amqplib/amqp.inc | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/library/php-amqplib/amqp.inc b/airtime_mvc/library/php-amqplib/amqp.inc
index 1bf03b3c4..57c642abd 100644
--- a/airtime_mvc/library/php-amqplib/amqp.inc
+++ b/airtime_mvc/library/php-amqplib/amqp.inc
@@ -463,8 +463,10 @@ class AMQPConnection extends AbstractChannel
             {
               debug_msg("closing socket");
             }
-            
-            @fclose($this->sock);
+
+            if (is_resource($this->sock)) { 
+                @fclose($this->sock);
+            }
             $this->sock = NULL;
         }
     }

From 25d6ecac0eaec9633f66b539aa5b14a6aa9727bc Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 16:54:57 -0400
Subject: [PATCH 18/34] Disabled broken maintenance screen code to squash
 warnings on upgrade

---
 airtime_mvc/application/upgrade/Upgrades.php | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 4535fe475..0b0edb442 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -38,14 +38,17 @@ abstract class AirtimeUpgrader
             //create a temporary maintenance notification file
             //when this file is on the server, zend framework redirects all
             //requests to the maintenance page and sets a 503 response code
+            /* DISABLED because this does not work correctly
             $this->maintenanceFile = isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."maintenance.txt" : "/tmp/maintenance.txt";
             $file = fopen($this->maintenanceFile, 'w');
             fclose($file);
+             */
         } else {
             //delete maintenance.txt to give users access back to Airtime
+            /* DISABLED because this does not work correctly
             if ($this->maintenanceFile) {
                 unlink($this->maintenanceFile);
-            }
+            }*/
         }
     }
             

From 48b4eaacd6b35d07de145b9b2fdec5603ec8dc12 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 24 Mar 2015 17:02:04 -0400
Subject: [PATCH 19/34] Squashed ob_end_flush(): failed to delete and flush
 buffer warning

---
 airtime_mvc/application/common/FileIO.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php
index 2c7724757..4d1ffd68c 100644
--- a/airtime_mvc/application/common/FileIO.php
+++ b/airtime_mvc/application/common/FileIO.php
@@ -65,7 +65,9 @@ class Application_Common_FileIO
         //We can have multiple levels of output buffering. Need to
         //keep looping until all have been disabled!!!
         //http://www.php.net/manual/en/function.ob-end-flush.php
-        while (@ob_end_flush());
+        while (ob_get_level() > 0) {
+            ob_end_flush();
+        }
 
         // NOTE: We can't use fseek here because it does not work with streams
         // (a.k.a. Files stored in the cloud)

From 99d16444d26633d154cb0ed75e6ca20d85e084b3 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Wed, 25 Mar 2015 10:51:51 -0400
Subject: [PATCH 20/34] Squash unlink warning in CcFiles.php

---
 airtime_mvc/application/models/airtime/CcFiles.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php
index 029d21a15..ff9d3d7fc 100644
--- a/airtime_mvc/application/models/airtime/CcFiles.php
+++ b/airtime_mvc/application/models/airtime/CcFiles.php
@@ -95,9 +95,10 @@ class CcFiles extends BaseCcFiles {
 
         try {
             self::createAndImport($fileArray, $tempFilePath, $originalFilename);
-        } catch (Exception $e)
-        {
-            @unlink($tempFilePath);
+        } catch (Exception $e) {
+            if (file_exists($tempFilePath)) {
+                unlink($tempFilePath);
+            }
             throw $e;
         }
     }

From 040eb1443db9512361b05b8464c17089780ca5ff Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 25 Mar 2015 17:29:28 -0400
Subject: [PATCH 21/34] CSS fixes to make page header more responsive

---
 airtime_mvc/application/configs/conf.php      |  2 +-
 .../application/layouts/scripts/layout.phtml  | 25 +++---
 .../application/views/scripts/menu.phtml      |  4 +-
 airtime_mvc/public/css/masterpanel.css        | 13 ++--
 airtime_mvc/public/css/styles.css             | 77 ++++++++++++++++---
 5 files changed, 90 insertions(+), 31 deletions(-)

diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index f89a5472e..e0ec1b952 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -99,7 +99,7 @@ class Config {
     public static function setAirtimeVersion() {
         $airtime_version = Application_Model_Preference::GetAirtimeVersion();
         $uniqueid = Application_Model_Preference::GetUniqueId();
-        $buildVersion = @file_get_contents(self::$rootDir."/../VERSION");
+        $buildVersion = "2.5.2"; // @file_get_contents(self::$rootDir."/../VERSION");
         self::$CC_CONFIG['airtime_version'] = md5($airtime_version.$buildVersion);
     }
     
diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index df5d0281b..bf97086c5 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -21,7 +21,6 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
 
 <?php echo $this->partial('partialviews/trialBox.phtml', array("is_trial"=>$this->isTrial(), "trial_remain"=> $this->trialRemaining())) ?>
 <div id="Panel">
-    <div class="logo"></div>
     <?php echo $this->versionNotify();
             $sss = $this->SourceSwitchStatus();
             $scs = $this->SourceConnectionStatus();
@@ -30,17 +29,21 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                 "scheduled_play_switch"=>$sss['scheduled_play'])) ?>
 <?php 	$partial = array('menu.phtml', 'default');
     $this->navigation()->menu()->setPartial($partial); ?>
-    <div class="personal-block solo">
-        <ul>
-          <li>
-            <!-- <span class="trial-box-button"><a title="Billing" href=<?php echo $baseUrl . 'billing/upgrade'?>>Upgrade</a></span>&nbsp;-->
-            <a id="current-user" href=<?php echo $baseUrl . "User/edit-user"?>><span class="name"><?php echo $this->escape($this->loggedInAs()); ?></span></a>
-             | <a href=<?php echo $baseUrl . "Login/logout"?>><?php echo _("Logout")?></a>
-          </li>
-        </ul>
-    </div>
+    <div id="nav">
+        <div class="logo"></div>
+        <div class="personal-block solo">
+            <ol>
+              <li>
+                <!-- <span class="trial-box-button"><a title="Billing" href=<?php echo $baseUrl . 'billing/upgrade'?>>Upgrade</a></span>&nbsp;-->
+                <a id="current-user" href=<?php echo $baseUrl . "User/edit-user"?>><span class="name"><?php echo $this->escape($this->loggedInAs()); ?></span></a>
+                 | <a href=<?php echo $baseUrl . "Login/logout"?>><?php echo _("Logout")?></a>
+              </li>
+            </ol>
+        </div>
 
-<?php echo $this->navigation()->menu() ?>
+        <?php echo $this->navigation()->menu() ?>
+        <div style="clear:both;"></div>
+    </div>
 </div>
 
 <div class="wrapper" id="content"><?php echo $this->layout()->content ?></div>
diff --git a/airtime_mvc/application/views/scripts/menu.phtml b/airtime_mvc/application/views/scripts/menu.phtml
index 64af78861..1cd0d7e04 100644
--- a/airtime_mvc/application/views/scripts/menu.phtml
+++ b/airtime_mvc/application/views/scripts/menu.phtml
@@ -1,4 +1,4 @@
-<ul id="nav">
+<ol id="navlist">
 <?php foreach ($this->container as $page) : ?>
     <?php if($this->navigation()->accept($page)) : ?>
 	    <li class="top <?php if($page->isActive(true)){echo 'active';} ?>">
@@ -29,4 +29,4 @@
 	    </li>
     <?php endif; ?>
 <?php endforeach; ?>
-</ul>
+</ol>
diff --git a/airtime_mvc/public/css/masterpanel.css b/airtime_mvc/public/css/masterpanel.css
index 39c95044f..d641bbb01 100644
--- a/airtime_mvc/public/css/masterpanel.css
+++ b/airtime_mvc/public/css/masterpanel.css
@@ -300,13 +300,12 @@
 
 
 .personal-block.solo {
-	position: absolute;
-	right: 145px;
-	top: 104px;
-	width: auto;
-	z-index: 1000;
-	height:auto;
-	margin:0;
+    width: auto;
+    height:auto;
+    margin: 0 10px 0 0;
+}
+.personal-block.solo ol {
+    margin-top: 6px;
 }
 .time-info-block.pull-right {
 	margin-right:0;
diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css
index 67eb50aaf..5b9ce8677 100644
--- a/airtime_mvc/public/css/styles.css
+++ b/airtime_mvc/public/css/styles.css
@@ -48,14 +48,12 @@ select {
 }
 
 .logo {
-    position:absolute;
-    right:20px;
-    top:104px;
-    background:transparent url(images/airtime_logo.png) no-repeat 0 0;
-    height:35px;
-    width:66px;
-    z-index:1000;
-    display:block;
+    background: transparent url(images/airtime_logo.png) no-repeat 0 0;
+    height: 35px;
+    width: 66px;
+    float: right;
+    padding: 0 5px 0 10px;
+    margin-top: -5px;
 }
 
 /* Version Notification Starts*/
@@ -286,9 +284,68 @@ select {
     background:url(images/masterpanel_spacer.png) no-repeat right 0;
 }
 .time-info-block  {
-    padding:0 14px 0 2px;
-    min-width:105px;
+    position: absolute;
+    top: 0;
+    right: 0;
 }
+#navlist {
+    padding: 0;
+    margin: 0;
+}
+#nav li.top {
+    float: none;
+}
+@media screen and (max-width: 1200px) {
+    .now-playing-block {
+        width: 30%;
+    }
+    .show-block {
+        width: 25%;
+    }
+}
+@media screen and (max-width: 920px) {
+    .now-playing-block {
+        width: 50%;
+    }
+    .show-block {
+        display: none;
+    }
+    .personal-block.solo {
+        right: 10px !important;
+    }
+}
+@media screen and (max-width: 810px) {
+    .now-playing-block {
+        width: 40%;
+    }
+}
+@media screen and (max-width: 863px) {
+    #nav {
+        height: inherit;
+        overflow-y: visible;
+    }
+}
+@media screen and (max-width: 680px) {
+    .now-playing-block {
+        display: none;
+    }
+    #nav li.top {
+        display: -webkit-flex;
+        width: 110px;
+    }
+    .personal-block.solo {
+        float: none;
+        text-align: left;
+    }
+    .personal-block.solo ol {
+        padding-left: 12px;
+    }
+    .logo {
+        float: none;
+        margin-left: 12px;
+    }
+}
+
 .time-info-block ul {
     margin:0;
     padding:6px 0 0;

From 58624ec100d62fd2d151a674f0eeec5b5a4cc0be Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Wed, 25 Mar 2015 17:34:38 -0400
Subject: [PATCH 22/34] Try to silence another permissions error problem

---
 airtime_mvc/application/models/StoredFile.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index e73d0a246..1049d449d 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -984,6 +984,11 @@ SQL;
         } else {
             Logging::info("Moving file $audio_file to $audio_stor");
 
+            //Ensure we have permissions to overwrite the file in stor, in case it already exists.
+            if (file_exists($audio_stor)) {
+                chmod($audio_stor, 0644);
+            }
+
             // Martin K.: changed to rename: Much less load + quicker since this is
             // an atomic operation
             if (rename($audio_file, $audio_stor) === false) {

From 669d9805655c2a54665b2253340f158dfa8323a9 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 25 Mar 2015 17:35:43 -0400
Subject: [PATCH 23/34] Fixed conf.php

---
 airtime_mvc/application/configs/conf.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index e0ec1b952..f89a5472e 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -99,7 +99,7 @@ class Config {
     public static function setAirtimeVersion() {
         $airtime_version = Application_Model_Preference::GetAirtimeVersion();
         $uniqueid = Application_Model_Preference::GetUniqueId();
-        $buildVersion = "2.5.2"; // @file_get_contents(self::$rootDir."/../VERSION");
+        $buildVersion = @file_get_contents(self::$rootDir."/../VERSION");
         self::$CC_CONFIG['airtime_version'] = md5($airtime_version.$buildVersion);
     }
     

From 271dc266faf55bcef7a15e1b9e006cb9ba020dc3 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Thu, 26 Mar 2015 12:08:52 -0400
Subject: [PATCH 24/34] S3 proxy cache support + 1 minor analyzer bugfix

---
 .../cloud_storage/Amazon_S3StorageBackend.php | 48 ++++++++++++++++---
 .../airtime_analyzer/message_listener.py      |  1 +
 2 files changed, 43 insertions(+), 6 deletions(-)

diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
index b51b0059f..46feaeb82 100644
--- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
@@ -9,20 +9,33 @@ class Amazon_S3StorageBackend extends StorageBackend
 {
     
     private $s3Client;
-    
+    private $proxyHost;
+
     public function Amazon_S3StorageBackend($securityCredentials)
     {
         $this->setBucket($securityCredentials['bucket']);
         $this->setAccessKey($securityCredentials['api_key']);
         $this->setSecretKey($securityCredentials['api_key_secret']);
 
-        $this->s3Client = S3Client::factory(array(
+        $s3Options = array(
             'key' => $securityCredentials['api_key'],
             'secret' => $securityCredentials['api_key_secret'],
             'region' => $securityCredentials['region']
-        ));
-    }
+        );
+        if (array_key_exists("proxy_host", $securityCredentials)) {
+            $s3Options = array_merge($s3Options, array(
+                //'base_url' => "http://" . $securityCredentials['proxy_host'],
+                'base_url' => "http://s3.amazonaws.com",
+                'scheme' => "http",
+                //'force_path_style'  => true,
+                'signature' => 'v4'
+            ));
+            $this->proxyHost = $securityCredentials['proxy_host'];
+        }
     
+        $this->s3Client = S3Client::factory($s3Options);
+    }
+
     public function getAbsoluteFilePath($resourceId)
     {
         return $this->s3Client->getObjectUrl($this->getBucket(), $resourceId);
@@ -30,9 +43,32 @@ class Amazon_S3StorageBackend extends StorageBackend
 
     public function getSignedURL($resourceId)
     {
-        return $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes');
+        $url = $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes');
+
+        //If we're using the proxy cache, we need to modify the request URL after it has
+        //been generated by the above. (The request signature must be for the amazonaws.com,
+        //not our proxy, since the proxy translates the host back to amazonaws.com)
+        if ($this->proxyHost) {
+            $p = parse_url($url);
+            $p["host"] = $this->getBucket() . "." . $this->proxyHost;
+            $p["scheme"] = "http";
+            //If the path contains the bucket name (which is the case with HTTPS requests to Amazon),
+            //we need to strip that part out, since we're forcing everything to HTTP. The Amazon S3
+            //URL convention for HTTP is to prepend the bucket name to the hostname instead of having
+            //it in the path.
+            //eg. http://bucket.s3.amazonaws.com/ instead of https://s3.amazonaws.com/bucket/
+            if (strpos($p["path"], $this->getBucket()) == 1) {
+                $p["path"] = substr($p["path"], 1 + strlen($this->getBucket()));
+            }
+            $url = $p["scheme"] . "://" . $p["host"] . $p["path"] . "?" . $p["query"];
+        }
+        //http_build_url() would be nice to use but it requires pecl_http :-(
+
+        Logging::info($url);
+
+        return $url;
     }
-    
+
     public function deletePhysicalFile($resourceId)
     {
         $bucket = $this->getBucket();
diff --git a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
index 111471153..89ca24fdf 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
@@ -226,6 +226,7 @@ class MessageListener:
         else:
             raise Exception("Analyzer process terminated unexpectedly.")
         '''
+        results = {}
 
         q = Queue.Queue()
         try:

From 3fe1a46c41ee18f5ad80ff6a07c5cc6ace421148 Mon Sep 17 00:00:00 2001
From: duncan <duncan.sommerville@gmail.com>
Date: Thu, 26 Mar 2015 12:50:53 -0400
Subject: [PATCH 25/34] Fixed dropdown menus not behaving correctly

---
 airtime_mvc/public/css/styles.css | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css
index 5b9ce8677..ca50bebd7 100644
--- a/airtime_mvc/public/css/styles.css
+++ b/airtime_mvc/public/css/styles.css
@@ -293,7 +293,7 @@ select {
     margin: 0;
 }
 #nav li.top {
-    float: none;
+    /*float: none;*/
 }
 @media screen and (max-width: 1200px) {
     .now-playing-block {
@@ -345,7 +345,14 @@ select {
         margin-left: 12px;
     }
 }
-
+@media screen and (max-width: 380px) {
+    .time-info-block {
+	display: none;
+    }
+    .on-air-block {
+	margin: 0;
+    }
+}
 .time-info-block ul {
     margin:0;
     padding:6px 0 0;

From d31de0937f2c671f1d22f4387d3dbd6402ee21a9 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Mon, 30 Mar 2015 11:31:07 -0400
Subject: [PATCH 26/34] Refactored file storage code slightly to allow multiple
 download URLs

---
 .../cloud_storage/Amazon_S3StorageBackend.php | 22 +++++++---
 .../cloud_storage/FileStorageBackend.php      |  2 +-
 .../cloud_storage/ProxyStorageBackend.php     |  4 +-
 .../cloud_storage/StorageBackend.php          |  2 +-
 airtime_mvc/application/common/FileIO.php     |  5 +--
 .../application/controllers/ApiController.php |  4 +-
 .../application/models/ShowInstance.php       |  7 +++-
 airtime_mvc/application/models/StoredFile.php | 17 ++++----
 .../application/models/airtime/CcFiles.php    |  4 +-
 .../application/models/airtime/CloudFile.php  |  4 +-
 .../application/services/MediaService.php     | 42 +++++++++++++++----
 11 files changed, 78 insertions(+), 35 deletions(-)

diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
index 46feaeb82..270015ae0 100644
--- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php
@@ -41,15 +41,19 @@ class Amazon_S3StorageBackend extends StorageBackend
         return $this->s3Client->getObjectUrl($this->getBucket(), $resourceId);
     }
 
-    public function getSignedURL($resourceId)
+    /** Returns a signed download URL from Amazon S3, expiring in 60 minutes */
+    public function getDownloadURLs($resourceId)
     {
-        $url = $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes');
+        $urls = array();
+
+        $signedS3Url = $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes');
+
 
         //If we're using the proxy cache, we need to modify the request URL after it has
         //been generated by the above. (The request signature must be for the amazonaws.com,
         //not our proxy, since the proxy translates the host back to amazonaws.com)
         if ($this->proxyHost) {
-            $p = parse_url($url);
+            $p = parse_url($signedS3Url);
             $p["host"] = $this->getBucket() . "." . $this->proxyHost;
             $p["scheme"] = "http";
             //If the path contains the bucket name (which is the case with HTTPS requests to Amazon),
@@ -60,13 +64,19 @@ class Amazon_S3StorageBackend extends StorageBackend
             if (strpos($p["path"], $this->getBucket()) == 1) {
                 $p["path"] = substr($p["path"], 1 + strlen($this->getBucket()));
             }
-            $url = $p["scheme"] . "://" . $p["host"] . $p["path"] . "?" . $p["query"];
+            $proxyUrl = $p["scheme"] . "://" . $p["host"] . $p["path"] . "?" . $p["query"];
+            //Add this proxy cache URL to the list of download URLs.
+            array_push($urls, $proxyUrl);
         }
+
+        //Add the direct S3 URL to the list (as a fallback)
+        array_push($urls, $signedS3Url);
+
         //http_build_url() would be nice to use but it requires pecl_http :-(
 
-        Logging::info($url);
+        //Logging::info($url);
 
-        return $url;
+        return $urls;
     }
 
     public function deletePhysicalFile($resourceId)
diff --git a/airtime_mvc/application/cloud_storage/FileStorageBackend.php b/airtime_mvc/application/cloud_storage/FileStorageBackend.php
index 81effde71..e7a87147e 100644
--- a/airtime_mvc/application/cloud_storage/FileStorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/FileStorageBackend.php
@@ -13,7 +13,7 @@ class FileStorageBackend extends StorageBackend
         return $resourceId;
     }
 
-    public function getSignedURL($resourceId)
+    public function getDownloadURLs($resourceId)
     {
         return "";
     }
diff --git a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php
index 4df1e19f7..d99c62eef 100644
--- a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php
@@ -38,9 +38,9 @@ class ProxyStorageBackend extends StorageBackend
         return $this->storageBackend->getAbsoluteFilePath($resourceId);
     }
 
-    public function getSignedURL($resourceId)
+    public function getDownloadURLs($resourceId)
     {
-        return $this->storageBackend->getSignedURL($resourceId);
+        return $this->storageBackend->getDownloadURLs($resourceId);
     }
 
     public function deletePhysicalFile($resourceId)
diff --git a/airtime_mvc/application/cloud_storage/StorageBackend.php b/airtime_mvc/application/cloud_storage/StorageBackend.php
index 028534e61..f0d58ba42 100644
--- a/airtime_mvc/application/cloud_storage/StorageBackend.php
+++ b/airtime_mvc/application/cloud_storage/StorageBackend.php
@@ -15,7 +15,7 @@ abstract class StorageBackend
     
     /** Returns the file object's signed URL. The URL must be signed since they
      *  privately stored on the storage backend. */
-    abstract public function getSignedURL($resourceId);
+    abstract public function getDownloadURLs($resourceId);
     
     /** Deletes the file from the storage backend. */
     abstract public function deletePhysicalFile($resourceId);
diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php
index 4d1ffd68c..e4210a387 100644
--- a/airtime_mvc/application/common/FileIO.php
+++ b/airtime_mvc/application/common/FileIO.php
@@ -10,7 +10,7 @@ class Application_Common_FileIO
      *
      * This HTTP_RANGE compatible read file function is necessary for allowing streaming media to be skipped around in.
      *
-     * @param string $filePath - the full filepath pointing to the location of the file
+     * @param string $filePath - the full filepath or URL pointing to the location of the file
      * @param string $mimeType - the file's mime type. Defaults to 'audio/mp3'
      * @param integer $size - the file size, in bytes
      * @return void
@@ -22,8 +22,7 @@ class Application_Common_FileIO
     {
         $fm = @fopen($filePath, 'rb');
         if (!$fm) {
-            header ("HTTP/1.1 505 Internal server error");
-            return;
+            throw new FileNotFoundException($filePath);
         }
 
         //Note that $size is allowed to be zero. If that's the case, it means we don't
diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php
index c90b41c22..a233a0089 100644
--- a/airtime_mvc/application/controllers/ApiController.php
+++ b/airtime_mvc/application/controllers/ApiController.php
@@ -1073,7 +1073,9 @@ class ApiController extends Zend_Controller_Action
                         $dir->getId(),$all=false);
                     foreach ($files as $f) {
                         // if the file is from this mount
-                        if (substr($f->getFilePath(), 0, strlen($rd)) === $rd) {
+                        $filePaths = $f->getFilePaths();
+                        $filePath = $filePaths[0];
+                        if (substr($filePath, 0, strlen($rd)) === $rd) {
                             $f->delete();
                         }
                     }
diff --git a/airtime_mvc/application/models/ShowInstance.php b/airtime_mvc/application/models/ShowInstance.php
index c15cdc631..e931b93b9 100644
--- a/airtime_mvc/application/models/ShowInstance.php
+++ b/airtime_mvc/application/models/ShowInstance.php
@@ -138,8 +138,11 @@ SQL;
         if (isset($file_id)) {
             $file =  Application_Model_StoredFile::RecallById($file_id);
 
-            if (isset($file) && file_exists($file->getFilePath())) {
-                return $file;
+            if (isset($file)) {
+                $filePaths = $file->getFilePaths();
+                if (file_exists($filePaths[0])) {
+                    return $file;
+                }
             }
         }
 
diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index e73d0a246..7d83d7a21 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -362,8 +362,9 @@ SQL;
     {
         $exists = false;
         try {
-            $filePath = $this->getFilePath();
-            $exists = (file_exists($this->getFilePath()) && !is_dir($filePath));
+            $filePaths = $this->getFilePaths();
+            $filePath = $filePaths[0];
+            $exists = (file_exists($filePath) && !is_dir($filePath));
         } catch (Exception $e) {
             return false;
         }
@@ -444,8 +445,6 @@ SQL;
      */
     public function deleteByMediaMonitor($deleteFromPlaylist=false)
     {
-        $filepath = $this->getFilePath();
-
         if ($deleteFromPlaylist) {
             Application_Model_Playlist::DeleteFileFromAllPlaylists($this->getId());
         }
@@ -499,13 +498,13 @@ SQL;
     /**
      * Get the absolute filepath
      *
-     * @return string
+     * @return array of strings
      */
-    public function getFilePath()
+    public function getFilePaths()
     {
         assert($this->_file);
         
-        return $this->_file->getURLForTrackPreviewOrDownload();
+        return $this->_file->getURLsForTrackPreviewOrDownload();
     }
 
     /**
@@ -1238,9 +1237,11 @@ SQL;
                 $genre       = $file->getDbGenre();
                 $release     = $file->getDbUtime();
                 try {
+                    $filePaths = $this->getFilePaths();
+                    $filePath = $filePaths[0];
                     $soundcloud     = new Application_Model_Soundcloud();
                     $soundcloud_res = $soundcloud->uploadTrack(
-                        $this->getFilePath(), $this->getName(), $description,
+                        $filePath, $this->getName(), $description,
                         $tag, $release, $genre);
                     $this->setSoundCloudFileId($soundcloud_res['id']);
                     $this->setSoundCloudLinkToFile($soundcloud_res['permalink_url']);
diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php
index ff9d3d7fc..4140485ea 100644
--- a/airtime_mvc/application/models/airtime/CcFiles.php
+++ b/airtime_mvc/application/models/airtime/CcFiles.php
@@ -386,9 +386,9 @@ class CcFiles extends BaseCcFiles {
     /**
      * Returns the file's absolute file path stored on disk.
      */
-    public function getURLForTrackPreviewOrDownload()
+    public function getURLsForTrackPreviewOrDownload()
     {
-        return $this->getAbsoluteFilePath();
+        return array($this->getAbsoluteFilePath());
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/CloudFile.php b/airtime_mvc/application/models/airtime/CloudFile.php
index 50b805ab1..3f0331ccc 100644
--- a/airtime_mvc/application/models/airtime/CloudFile.php
+++ b/airtime_mvc/application/models/airtime/CloudFile.php
@@ -27,12 +27,12 @@ class CloudFile extends BaseCloudFile
      * requesting the file's object via this URL, it needs to be signed because
      * all objects stored on Amazon S3 are private.
      */
-    public function getURLForTrackPreviewOrDownload()
+    public function getURLsForTrackPreviewOrDownload()
     {
         if ($this->proxyStorageBackend == null) {
             $this->proxyStorageBackend = new ProxyStorageBackend($this->getStorageBackend());
         }
-        return $this->proxyStorageBackend->getSignedURL($this->getResourceId());
+        return $this->proxyStorageBackend->getDownloadURLs($this->getResourceId());
     }
     
     /**
diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php
index ee4238e8d..7202fd120 100644
--- a/airtime_mvc/application/services/MediaService.php
+++ b/airtime_mvc/application/services/MediaService.php
@@ -55,10 +55,11 @@ class Application_Service_MediaService
         if ($media == null) {
             throw new FileNotFoundException();
         }
-        $filepath = $media->getFilePath();
-        // Make sure we don't have some wrong result beecause of caching
+        // Make sure we don't have some wrong result because of caching
         clearstatcache();
 
+        $filePath = "";
+
         if ($media->getPropelOrm()->isValidPhysicalFile()) {
             $filename = $media->getPropelOrm()->getFilename();
             //Download user left clicks a track and selects Download.
@@ -71,13 +72,40 @@ class Application_Service_MediaService
                 header('Content-Disposition: inline; filename="' . $filename . '"');
             }
 
-            $filepath = $media->getFilePath();
-            $size= $media->getFileSize();
-            $mimeType = $media->getPropelOrm()->getDbMime();
-            Application_Common_FileIO::smartReadFile($filepath, $size, $mimeType);
+            /*
+            In this block of code below, we're getting the list of download URLs for a track
+            and then streaming the file as the response. A file can be stored in more than one location,
+            with the alternate locations used as a fallback, so that's why we're looping until we
+            are able to actually send the file.
+
+            This mechanism is used to try fetching our file from our internal S3 caching proxy server first.
+            If the file isn't found there (or the cache is down), then we attempt to download the file
+            directly from Amazon S3. We do this to save bandwidth costs!
+            */
+
+            $filePaths = $media->getFilePaths();
+            assert(is_array($filePaths));
+
+            do {
+                //Read from $filePath and stream it to the browser.
+                $filePath = array_shift($filePaths);
+                try {
+                    $size= $media->getFileSize();
+                    $mimeType = $media->getPropelOrm()->getDbMime();
+                    Application_Common_FileIO::smartReadFile($filePath, $size, $mimeType);
+                } catch (FileNotFoundException $e) {
+                    //If we have no alternate filepaths left, then let the exception bubble up.
+                    if (sizeof($filePaths) == 0) {
+                        throw $e;
+                    }
+                }
+                //Retry with the next alternate filepath in the list
+            } while (sizeof($filePaths) > 0);
+
             exit;
+
         } else {
-            throw new FileNotFoundException();
+            throw new FileNotFoundException($filePath);
         }
     }
 

From ae02fe7816f5da4e53671a031a2f38ea5fd269fb Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Mon, 30 Mar 2015 13:21:26 -0400
Subject: [PATCH 27/34] Log traceback in analyzer status_reporter.py exception
 handler

---
 python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
index 9c2d52a31..23c6175c3 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
@@ -129,6 +129,7 @@ def send_http_request(picklable_request, retry_queue):
         retry_queue.append(picklable_request) # Retry it later
     except Exception as e:
         logging.error("HTTP request failed with unhandled exception. %s" % str(e))
+        logging.error(traceback.format_exc())
         # Don't put the request into the retry queue, just give up on this one.
         # I'm doing this to protect against us getting some pathological request
         # that breaks our code. I don't want us pickling data that potentially

From 79d2ae12fb2ab3844b87c77d41d8437a8beba408 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 31 Mar 2015 12:38:42 -0400
Subject: [PATCH 28/34] Added 2.5.12 upgrader and increased show/show instance
 description field length

---
 .../controllers/UpgradeController.php         |  3 +-
 .../upgrade_sql/airtime_2.5.12/upgrade.sql    |  2 +
 airtime_mvc/application/upgrade/Upgrades.php  | 49 +++++++++++++++++++
 airtime_mvc/build/schema.xml                  |  4 +-
 airtime_mvc/build/sql/schema.sql              |  4 +-
 5 files changed, 57 insertions(+), 5 deletions(-)
 create mode 100644 airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql

diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php
index 9af3dc1e9..518e173ac 100644
--- a/airtime_mvc/application/controllers/UpgradeController.php
+++ b/airtime_mvc/application/controllers/UpgradeController.php
@@ -20,7 +20,8 @@ class UpgradeController extends Zend_Controller_Action
         array_push($upgraders, new AirtimeUpgrader259());
         array_push($upgraders, new AirtimeUpgrader2510());
         array_push($upgraders, new AirtimeUpgrader2511());
-        
+        array_push($upgraders, new AirtimeUpgrader2512());
+
         $didWePerformAnUpgrade = false;
         try 
         {
diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql
new file mode 100644
index 000000000..92ca30ade
--- /dev/null
+++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql
@@ -0,0 +1,2 @@
+ALTER TABLE cc_show ALTER COLUMN description TYPE varchar(8192);
+ALTER TABLE cc_show_instances ALTER COLUMN description TYPE varchar(8192);
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 0b0edb442..9468753d6 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -388,3 +388,52 @@ class AirtimeUpgrader2511 extends AirtimeUpgrader
 
     }
 }
+
+class AirtimeUpgrader2512 extends AirtimeUpgrader
+{
+    protected function getSupportedVersions() {
+        return array (
+            '2.5.10',
+            '2.5.11'
+        );
+    }
+
+    public function getNewVersion() {
+        return '2.5.12';
+    }
+
+    public function upgrade($dir = __DIR__) {
+        Cache::clear();
+        assert($this->checkIfUpgradeSupported());
+
+        $newVersion = $this->getNewVersion();
+
+        try {
+            $this->toggleMaintenanceScreen(true);
+            Cache::clear();
+
+            // Begin upgrade
+            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
+            $values = parse_ini_file($airtimeConf, true);
+
+            $username = $values['database']['dbuser'];
+            $password = $values['database']['dbpass'];
+            $host = $values['database']['host'];
+            $database = $values['database']['dbname'];
+
+            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
+                .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+
+            Application_Model_Preference::SetAirtimeVersion($newVersion);
+            Cache::clear();
+
+            $this->toggleMaintenanceScreen(false);
+        } catch(Exception $e) {
+            $this->toggleMaintenanceScreen(false);
+            throw $e;
+        }
+    }
+    public function downgrade() {
+
+    }
+}
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index ef382454c..ece788ea7 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -139,7 +139,7 @@
     <column name="name" phpName="DbName" type="VARCHAR" size="255" required="true" defaultValue=""/>
     <column name="url" phpName="DbUrl" type="VARCHAR" size="255" required="false" defaultValue=""/>
     <column name="genre" phpName="DbGenre" type="VARCHAR" size="255" required="false" defaultValue=""/>
-    <column name="description" phpName="DbDescription" type="VARCHAR" size="512" required="false"/>
+    <column name="description" phpName="DbDescription" type="VARCHAR" size="8192" required="false"/>
 	<column name="color" phpName="DbColor" type="VARCHAR" size="6" required="false"/>
 	<column name="background_color" phpName="DbBackgroundColor" type="VARCHAR" size="6" required="false"/>
 	<column name="live_stream_using_airtime_auth" phpName="DbLiveStreamUsingAirtimeAuth" type="BOOLEAN" required="false" defaultValue="false"/>
@@ -156,7 +156,7 @@
   </table>
   <table name="cc_show_instances" phpName="CcShowInstances">
     <column name="id" phpName="DbId" type="INTEGER" primaryKey="true" autoIncrement="true" required="true"/>
-    <column name="description" phpName="DbDescription" type="VARCHAR" size="512" required="false" defaultValue=""/>
+    <column name="description" phpName="DbDescription" type="VARCHAR" size="8192" required="false" defaultValue=""/>
     <column name="starts" phpName="DbStarts" type="TIMESTAMP" required="true"/>
     <column name="ends" phpName="DbEnds" type="TIMESTAMP" required="true"/>
 	<column name="show_id" phpName="DbShowId" type="INTEGER" required="true"/>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index a617ebda8..392539496 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -149,7 +149,7 @@ CREATE TABLE "cc_show"
     "name" VARCHAR(255) DEFAULT '' NOT NULL,
     "url" VARCHAR(255) DEFAULT '',
     "genre" VARCHAR(255) DEFAULT '',
-    "description" VARCHAR(512),
+    "description" VARCHAR(8192),
     "color" VARCHAR(6),
     "background_color" VARCHAR(6),
     "live_stream_using_airtime_auth" BOOLEAN DEFAULT 'f',
@@ -171,7 +171,7 @@ DROP TABLE IF EXISTS "cc_show_instances" CASCADE;
 CREATE TABLE "cc_show_instances"
 (
     "id" serial NOT NULL,
-    "description" VARCHAR(512) DEFAULT '',
+    "description" VARCHAR(8192) DEFAULT '',
     "starts" TIMESTAMP NOT NULL,
     "ends" TIMESTAMP NOT NULL,
     "show_id" INTEGER NOT NULL,

From dab9cc775ce523c0523e6b88338f15d3442d71dd Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 31 Mar 2015 15:39:06 -0400
Subject: [PATCH 29/34] Hide the show form after updating a show because it was
 broken at that point anyways

---
 airtime_mvc/public/js/airtime/schedule/add-show.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/schedule/add-show.js b/airtime_mvc/public/js/airtime/schedule/add-show.js
index a8c9f76e7..df862e971 100644
--- a/airtime_mvc/public/js/airtime/schedule/add-show.js
+++ b/airtime_mvc/public/js/airtime/schedule/add-show.js
@@ -82,7 +82,7 @@ function closeAddShowForm(event) {
         
         redrawAddShowForm($el, json.form);
     });
-    
+
     makeAddShowButton();
 }
 
@@ -742,7 +742,7 @@ function setAddShowEvents(form) {
             image = new FormData();
             image.append('file', $('#add_show_logo')[0].files[0]);
         }
-        
+
         $.ajax({
             url: action, 
             data: {format: "json", data: data, hosts: hosts, days: days},
@@ -784,6 +784,7 @@ function setAddShowEvents(form) {
                 } else {
                     redrawAddShowForm($addShowForm, json.newForm);
                     scheduleRefetchEvents(json);
+                    $addShowForm.hide();
                 }
             }
         });

From 69b03cdefaff36fee0dceb36568976e316206967 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Wed, 1 Apr 2015 16:16:46 -0400
Subject: [PATCH 30/34] Three small bugfixes

* Remove files from the database even if they couldn't be removed from disk. (log a warning)
* Return a better error message if the user attempts to delete a scheduled file
* Attempt to squash headers already sent warning during buffer flushing
  in FileIO.php
---
 airtime_mvc/application/common/FileIO.php                | 3 +++
 .../application/controllers/LibraryController.php        | 2 ++
 airtime_mvc/application/models/StoredFile.php            | 9 ++++++++-
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php
index e4210a387..4a99d4534 100644
--- a/airtime_mvc/application/common/FileIO.php
+++ b/airtime_mvc/application/common/FileIO.php
@@ -61,6 +61,9 @@ class Application_Common_FileIO
         }
         header("Content-Transfer-Encoding: binary");
 
+        //Squashes headers() warning on PHP 5.3/ubuntu 12.04:
+        flush();
+
         //We can have multiple levels of output buffering. Need to
         //keep looping until all have been disabled!!!
         //http://www.php.net/manual/en/function.ob-end-flush.php
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 8c635baa6..212d57521 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -356,6 +356,8 @@ class LibraryController extends Zend_Controller_Action
                     $res = $file->delete();
                 } catch (FileNoPermissionException $e) {
                     $message = $noPermissionMsg;
+                } catch (DeleteScheduledFileException $e) {
+                    $message = _("Could not delete file because it is scheduled in the future.");
                 } catch (Exception $e) {
                     //could throw a scheduled in future exception.
                     $message = _("Could not delete file(s).");
diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index 0f91c3f95..8e3dd7a59 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -400,7 +400,14 @@ SQL;
         //Delete the physical file from either the local stor directory
         //or from the cloud
         if ($this->_file->getDbImportStatus() == CcFiles::IMPORT_STATUS_SUCCESS) {
-            $this->_file->deletePhysicalFile();
+            try {
+                $this->_file->deletePhysicalFile();
+            }
+            catch (Exception $e)
+            {
+                //Just log the exception and continue.
+                Logging::error($e);
+            }
         }
 
         //Update the user's disk usage

From 9b85fc59a6c33a99e08c04dd0a9b05d8f54bf999 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Wed, 1 Apr 2015 16:29:59 -0400
Subject: [PATCH 31/34] Another attempt at squashing header() warning

---
 airtime_mvc/application/common/FileIO.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php
index 4a99d4534..170a5d8b9 100644
--- a/airtime_mvc/application/common/FileIO.php
+++ b/airtime_mvc/application/common/FileIO.php
@@ -35,6 +35,8 @@ class Application_Common_FileIO
         $begin = 0;
         $end   = $size - 1;
 
+        ob_start(); //Must start a buffer here for these header() functions
+
         if (isset($_SERVER['HTTP_RANGE'])) {
             if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
                 $begin = intval($matches[1]);
@@ -50,6 +52,7 @@ class Application_Common_FileIO
             header('HTTP/1.1 200 OK');
         }
         header("Content-Type: $mimeType");
+        header("Content-Transfer-Encoding: binary");
         header('Cache-Control: public, must-revalidate, max-age=0');
         header('Pragma: no-cache');
         header('Accept-Ranges: bytes');
@@ -59,7 +62,6 @@ class Application_Common_FileIO
                 header("Content-Range: bytes $begin-$end/$size");
             }
         }
-        header("Content-Transfer-Encoding: binary");
 
         //Squashes headers() warning on PHP 5.3/ubuntu 12.04:
         flush();

From 3e2cd54be7002066c961a04d816be06de9063d4b Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Wed, 1 Apr 2015 17:29:21 -0400
Subject: [PATCH 32/34] Fixed double sending of headers problem with S3 cache

---
 airtime_mvc/application/common/FileIO.php         | 3 ---
 airtime_mvc/application/services/MediaService.php | 1 +
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php
index 170a5d8b9..e921ca3c1 100644
--- a/airtime_mvc/application/common/FileIO.php
+++ b/airtime_mvc/application/common/FileIO.php
@@ -63,9 +63,6 @@ class Application_Common_FileIO
             }
         }
 
-        //Squashes headers() warning on PHP 5.3/ubuntu 12.04:
-        flush();
-
         //We can have multiple levels of output buffering. Need to
         //keep looping until all have been disabled!!!
         //http://www.php.net/manual/en/function.ob-end-flush.php
diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php
index 7202fd120..55eaeff37 100644
--- a/airtime_mvc/application/services/MediaService.php
+++ b/airtime_mvc/application/services/MediaService.php
@@ -93,6 +93,7 @@ class Application_Service_MediaService
                     $size= $media->getFileSize();
                     $mimeType = $media->getPropelOrm()->getDbMime();
                     Application_Common_FileIO::smartReadFile($filePath, $size, $mimeType);
+                    break; //Break out of the loop if we successfully read the file!
                 } catch (FileNotFoundException $e) {
                     //If we have no alternate filepaths left, then let the exception bubble up.
                     if (sizeof($filePaths) == 0) {

From 492a7f329a68c97951b3eccc27ef9981d193ef0d Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Mon, 6 Apr 2015 17:22:13 -0400
Subject: [PATCH 33/34] Minor airtime_analyzer error handling improvements and
 documentation

---
 .../airtime_analyzer/airtime_analyzer/message_listener.py       | 2 +-
 .../airtime_analyzer/airtime_analyzer/playability_analyzer.py   | 2 +-
 .../airtime_analyzer/airtime_analyzer/status_reporter.py        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
index 89ca24fdf..642e96f3f 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
@@ -233,7 +233,7 @@ class MessageListener:
             AnalyzerPipeline.run_analysis(q, audio_file_path, import_directory, original_filename, storage_backend, file_prefix, cloud_storage_config)
             results = q.get()
         except Exception as e:
-            logging.error("Analyzer pipeline exception", e)
+            logging.error("Analyzer pipeline exception: %s" % str(e))
             pass
 
         # Ensure our queue doesn't fill up and block due to unexpected behaviour. Defensive code.
diff --git a/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py
index 0ca8a84c1..eb9062713 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py
@@ -27,6 +27,6 @@ class PlayabilityAnalyzer(Analyzer):
             logging.warn("Failed to run: %s - %s. %s" % (command[0], e.strerror, "Do you have liquidsoap installed?"))
         except (subprocess.CalledProcessError, Exception) as e: # liquidsoap returned an error code
             logging.warn(e)
-            raise UnplayableFileError
+            raise UnplayableFileError()
 
         return metadata
diff --git a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
index 23c6175c3..88fb6ff28 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/status_reporter.py
@@ -25,7 +25,7 @@ class PicklableHttpRequest:
                                 auth=requests.auth.HTTPBasicAuth(self.api_key, ''))
 
 def process_http_requests(ipc_queue, http_retry_queue_path):
-    ''' Runs in a separate process and performs all the HTTP requests where we're
+    ''' Runs in a separate thread and performs all the HTTP requests where we're
         reporting extracted audio file metadata or errors back to the Airtime web application.
 
         This process also checks every 5 seconds if there's failed HTTP requests that we 

From d5012c25cb826bf2cc5bde6789e37395487b802b Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Mon, 6 Apr 2015 17:33:08 -0400
Subject: [PATCH 34/34] Another small bugfix for error handling in the analyzer

---
 .../airtime_analyzer/analyzer_pipeline.py                 | 8 +++++---
 .../airtime_analyzer/airtime_analyzer/message_listener.py | 8 ++++----
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py
index 206e6232e..04dadbed6 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py
@@ -21,7 +21,9 @@ class AnalyzerPipeline:
         so that if it crashes, it does not kill the entire airtime_analyzer daemon and
         the failure to import can be reported back to the web application.
     """
-    
+
+    IMPORT_STATUS_FAILED = 2
+
     @staticmethod
     def run_analysis(queue, audio_file_path, import_directory, original_filename, storage_backend, file_prefix, cloud_storage_config):
         """Analyze and import an audio file, and put all extracted metadata into queue.
@@ -86,12 +88,12 @@ class AnalyzerPipeline:
             queue.put(metadata)
         except UnplayableFileError as e:
             logging.exception(e)
-            metadata["import_status"] = 2
+            metadata["import_status"] = IMPORT_STATUS_FAILED
             metadata["reason"] = "The file could not be played."
             raise e
         except Exception as e:
             # Ensures the traceback for this child process gets written to our log files:
-            logging.exception(e) 
+            logging.exception(e)
             raise e
 
     @staticmethod
diff --git a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
index 642e96f3f..17d749a56 100644
--- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
+++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py
@@ -226,19 +226,19 @@ class MessageListener:
         else:
             raise Exception("Analyzer process terminated unexpectedly.")
         '''
-        results = {}
+        metadata = {}
 
         q = Queue.Queue()
         try:
             AnalyzerPipeline.run_analysis(q, audio_file_path, import_directory, original_filename, storage_backend, file_prefix, cloud_storage_config)
-            results = q.get()
+            metadata = q.get()
         except Exception as e:
             logging.error("Analyzer pipeline exception: %s" % str(e))
-            pass
+            metadata["import_status"] = AnalyzerPipeline.IMPORT_STATUS_FAILED
 
         # Ensure our queue doesn't fill up and block due to unexpected behaviour. Defensive code.
         while not q.empty():
             q.get()
 
-        return results
+        return metadata