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> --> - <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> --> + <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