Merge branch 'saas' into saas-speedy

This commit is contained in:
Albert Santoni 2015-04-09 12:20:23 -04:00
commit 3e255bfd37
50 changed files with 829 additions and 270 deletions

View File

@ -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();

View File

@ -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;
}
}

View File

@ -13,7 +13,7 @@ class FileStorageBackend extends StorageBackend
return $resourceId;
}
public function getSignedURL($resourceId)
public function getDownloadURLs($resourceId)
{
return "";
}

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -0,0 +1,92 @@
<?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() &&
!$user->isSourcefabricAdmin() &&
$request->getControllerKey() !== "thank-you")
{
//Only tracking trial->paid conversions for now.
if ($oldPlan == "trial")
{
return true;
}
}
return false;
}
}

View File

@ -27,7 +27,7 @@ class Application_Common_HTTPHelper
if (empty($baseDir)) {
$baseDir = "/";
}
if ($baseDir[0] != "") {
if ($baseDir[0] != "/") {
$baseDir = "/" . $baseDir;
}

View File

@ -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);
}
}
}

View File

@ -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');

View File

@ -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)) {

View File

@ -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();
}
}

View File

@ -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.');
}
}

View File

@ -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).");

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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
{

View File

@ -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()) {

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -38,7 +38,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();
@ -47,17 +46,21 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
"scheduled_play_switch"=>$sss['scheduled_play'])) ?>
<?php $partial = array('menu.phtml', 'default');
$this->navigation()->menu()->setPartial($partial); ?>
<div class="personal-block solo">
<ul>
<li>
<!-- <span class="trial-box-button"><a title="Billing" href=<?php echo $baseUrl . 'billing/upgrade'?>>Upgrade</a></span>&nbsp;-->
<a id="current-user" href=<?php echo $baseUrl . "User/edit-user"?>><span class="name"><?php echo $this->escape($this->loggedInAs()); ?></span></a>
| <a href=<?php echo $baseUrl . "Login/logout"?>><?php echo _("Logout")?></a>
</li>
</ul>
</div>
<div id="nav">
<div class="logo"></div>
<div class="personal-block solo">
<ol>
<li>
<!-- <span class="trial-box-button"><a title="Billing" href=<?php echo $baseUrl . 'billing/upgrade'?>>Upgrade</a></span>&nbsp;-->
<a id="current-user" href=<?php echo $baseUrl . "User/edit-user"?>><span class="name"><?php echo $this->escape($this->loggedInAs()); ?></span></a>
| <a href=<?php echo $baseUrl . "Login/logout"?>><?php echo _("Logout")?></a>
</li>
</ol>
</div>
<?php echo $this->navigation()->menu() ?>
<?php echo $this->navigation()->menu() ?>
<div style="clear:both;"></div>
</div>
</div>
<div class="wrapper" id="content"><?php echo $this->layout()->content ?></div>

View File

@ -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:

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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']);

View File

@ -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)

View File

@ -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());
}
/**

View File

@ -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());
}
/**

View File

@ -124,6 +124,7 @@ class Rest_MediaController extends Zend_Rest_Controller
catch (Exception $e) {
$this->unknownErrorResponse();
Logging::error($e->getMessage());
throw $e;
}
}

View File

@ -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);
}
}

View File

@ -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() {
}
}

View File

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

View File

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

View File

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

View File

@ -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">

View File

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

View File

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

View File

@ -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"/>

View File

@ -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,

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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;

View File

@ -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;
}

View File

@ -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();
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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//','/''}