diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 2c8cd839c..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'; @@ -37,7 +39,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(); @@ -52,6 +54,8 @@ 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 Application_Model_Locale::configureLocalization(); diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php index e2b6a1ba6..270015ae0 100644 --- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php +++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php @@ -9,30 +9,76 @@ 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); } - public function getSignedURL($resourceId) + /** Returns a signed download URL from Amazon S3, expiring in 60 minutes */ + public function getDownloadURLs($resourceId) { - return $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($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), + //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())); + } + $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); + + return $urls; } - + public function deletePhysicalFile($resourceId) { $bucket = $this->getBucket(); @@ -53,14 +99,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; } } 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 2c7724757..e921ca3c1 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 @@ -36,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]); @@ -51,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'); @@ -60,12 +62,13 @@ class Application_Common_FileIO header("Content-Range: bytes $begin-$end/$size"); } } - header("Content-Transfer-Encoding: binary"); //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) diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php new file mode 100644 index 000000000..00a95a51e --- /dev/null +++ b/airtime_mvc/application/common/GoogleAnalytics.php @@ -0,0 +1,92 @@ +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() && + !$user->isSourcefabricAdmin() && + $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/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php index 48bbddc8c..580c20e52 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,34 +35,37 @@ class ProvisioningHelper try { $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); - Logging::error($e->getMessage() - ); + Logging::error($e->getMessage()); echo $e->getMessage() . PHP_EOL; return; } @@ -94,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']; @@ -102,6 +107,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 +119,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 +138,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 +190,16 @@ class ProvisioningHelper $musicDir->save(); } + /** + * Initialize preference values passed from the dashboard (if any exist) + */ + private function initializePrefs() { + if ($this->station_name) { + Application_Model_Preference::SetStationName($this->station_name); + } + if ($this->description) { + Application_Model_Preference::SetStationDescription($this->description); + } + } } 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/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/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 @@ 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/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 5bafc1438..a42170d6d 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/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php index fc0c28cbb..4aa02fa39 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, false, $this)) { + return; + } + + try { + // This is hacky and should be genericized + if ($_POST['station_name']) { + Application_Model_Preference::SetStationName($_POST['station_name']); + } + if ($_POST['description']) { + Application_Model_Preference::SetStationDescription($_POST['description']); + } + } 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 */ @@ -31,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/ShowbuilderController.php b/airtime_mvc/application/controllers/ShowbuilderController.php index 65c2bb355..075281761 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'); @@ -369,104 +369,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 @@ +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/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/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/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 @@ +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/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/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml index 709d32b26..93c758c28 100644 --- a/airtime_mvc/application/layouts/scripts/layout.phtml +++ b/airtime_mvc/application/layouts/scripts/layout.phtml @@ -38,7 +38,6 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= partial('partialviews/trialBox.phtml', array("is_trial"=>$this->isTrial(), "trial_remain"=> $this->trialRemaining())) ?>
- versionNotify(); $sss = $this->SourceSwitchStatus(); $scs = $this->SourceConnectionStatus(); @@ -47,17 +46,21 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= "scheduled_play_switch"=>$sss['scheduled_play'])) ?> navigation()->menu()->setPartial($partial); ?> -
- -
+
layout()->content ?>
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/Preference.php b/airtime_mvc/application/models/Preference.php index aef7f0bb5..bc0b2006e 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); @@ -819,7 +824,10 @@ class Application_Model_Preference public static function SetPlanLevel($plan) { + $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); } public static function GetPlanLevel() @@ -832,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/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 5e6e53161..8e3dd7a59 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; } @@ -399,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 @@ -444,8 +452,6 @@ SQL; */ public function deleteByMediaMonitor($deleteFromPlaylist=false) { - $filepath = $this->getFilePath(); - if ($deleteFromPlaylist) { Application_Model_Playlist::DeleteFileFromAllPlaylists($this->getId()); } @@ -499,13 +505,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(); } /** @@ -984,15 +990,19 @@ 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) { + 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 " @@ -1239,9 +1249,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/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) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 029d21a15..4140485ea 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; } } @@ -385,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/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/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php index ee4238e8d..55eaeff37 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,41 @@ 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); + 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) { + throw $e; + } + } + //Retry with the next alternate filepath in the list + } while (sizeof($filePaths) > 0); + exit; + } else { - throw new FileNotFoundException(); + throw new FileNotFoundException($filePath); } } diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php index 31c602437..9468753d6 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); - } + }*/ } } @@ -381,4 +384,56 @@ class AirtimeUpgrader2511 extends AirtimeUpgrader throw $e; } } + public function downgrade() { + + } +} + +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/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 @@ + + + + + <?php echo _("An error has occurred.") ?> + headLink(); ?> + + +
+

+

+
+ +
+
+ + 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 @@ + + + + + <?php echo _("An error has occurred.") ?> + headLink(); ?> + + +
+

+

+
+ +
+
+ + 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 @@ + + + + + <?php echo _("An error has occurred.") ?> + headLink(); ?> + + +
+

+

+
+ +
+
+ + 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 @@ - <?php echo _("Zend Framework Default Application") ?> + <?php echo _("An error has occurred.") ?> + headLink(); ?>
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 @@ - + 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 @@ + +form->getElement('csrf') ?> + +
+
+
+

+

+

+
+
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 @@ - + @@ -156,7 +156,7 @@ - + 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, 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; } } diff --git a/airtime_mvc/public/css/images/maintenance.png b/airtime_mvc/public/css/images/maintenance.png new file mode 100644 index 000000000..0a000c6c6 Binary files /dev/null and b/airtime_mvc/public/css/images/maintenance.png differ 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 31a3f8383..ca50bebd7 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,8 +284,74 @@ 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; + } +} +@media screen and (max-width: 380px) { + .time-info-block { + display: none; + } + .on-air-block { + margin: 0; + } } .time-info-block ul { margin:0; @@ -899,20 +963,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 +1094,6 @@ input[type="checkbox"] { #pref_form p.description { color: #3b3b3b; font-size: 12px; - float: left; } dt.block-display, dd.block-display { @@ -2193,7 +2255,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 +2264,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 +2628,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 +2650,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 +3204,16 @@ dd .stream-status { } .quota-reached { font-size: 14px !important; +} + +.thankyou-panel +{ + width: 400px; + margin: 0 auto; + margin-bottom: 30px; +} + +.thankyou-panel h3 +{ + color: #222; +} 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(); } } }); 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 111471153..17d749a56 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py @@ -226,18 +226,19 @@ class MessageListener: else: raise Exception("Analyzer process terminated unexpectedly.") ''' + 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", e) - pass + logging.error("Analyzer pipeline exception: %s" % str(e)) + 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 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 9c2d52a31..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 @@ -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 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//','/''}