Merge branch 'saas' into saas-embed-player

This commit is contained in:
drigato 2015-03-11 17:13:04 -04:00
commit ac2181a52d
40 changed files with 913 additions and 608 deletions

View File

@ -20,6 +20,7 @@ require_once "DateHelper.php";
require_once "LocaleHelper.php"; require_once "LocaleHelper.php";
require_once "FileDataHelper.php"; require_once "FileDataHelper.php";
require_once "HTTPHelper.php"; require_once "HTTPHelper.php";
require_once "FileIO.php";
require_once "OsPath.php"; require_once "OsPath.php";
require_once "Database.php"; require_once "Database.php";
require_once "ProvisioningHelper.php"; require_once "ProvisioningHelper.php";
@ -82,13 +83,28 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
} }
$view->headScript()->appendScript("var userType = '$userType';"); $view->headScript()->appendScript("var userType = '$userType';");
} }
/**
* Create a global namespace to hold a session token for CSRF prevention
*/
protected function _initCsrfNamespace() {
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
// Check if the token exists
if (!$csrf_namespace->authtoken) {
// If we don't have a token, regenerate it and set a 2 hour timeout
// Should we log the user out here if the token is expired?
$csrf_namespace->authtoken = sha1(uniqid(rand(),1));
$csrf_namespace->setExpirationSeconds(2*60*60);
}
}
/** /**
* Ideally, globals should be written to a single js file once * Ideally, globals should be written to a single js file once
* from a php init function. This will save us from having to * from a php init function. This will save us from having to
* reinitialize them every request * reinitialize them every request
*/ */
private function _initTranslationGlobals($view) { private function _initTranslationGlobals() {
$view = $this->getResource('view');
$view->headScript()->appendScript("var PRODUCT_NAME = '" . PRODUCT_NAME . "';"); $view->headScript()->appendScript("var PRODUCT_NAME = '" . PRODUCT_NAME . "';");
$view->headScript()->appendScript("var USER_MANUAL_URL = '" . USER_MANUAL_URL . "';"); $view->headScript()->appendScript("var USER_MANUAL_URL = '" . USER_MANUAL_URL . "';");
$view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';"); $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';");
@ -174,9 +190,10 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
&& strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/audio-preview') === false && strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/audio-preview') === false
&& strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/playlist-preview') === false && strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/playlist-preview') === false
&& strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/block-preview') === false) { && strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/block-preview') === false) {
if (Application_Model_Preference::GetLiveChatEnabled()) { $plan_level = strval(Application_Model_Preference::GetPlanLevel());
// Since the Hobbyist plan doesn't come with Live Chat support, don't enable it
if (Application_Model_Preference::GetLiveChatEnabled() && $plan_level !== 'hobbyist') {
$client_id = strval(Application_Model_Preference::GetClientId()); $client_id = strval(Application_Model_Preference::GetClientId());
$plan_level = strval(Application_Model_Preference::GetPlanLevel());
$station_url = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; $station_url = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
$view->headScript()->appendScript("var livechat_client_id = '$client_id';\n". $view->headScript()->appendScript("var livechat_client_id = '$client_id';\n".
"var livechat_plan_type = '$plan_level';\n". "var livechat_plan_type = '$plan_level';\n".

View File

@ -60,8 +60,7 @@ class Amazon_S3StorageBackend extends StorageBackend
public function getFilePrefix() public function getFilePrefix()
{ {
$clientCurrentAirtimeProduct = Billing::getClientCurrentAirtimeProduct(); $hostingId = Billing::getClientInstanceId();
$hostingId = $clientCurrentAirtimeProduct["id"];
return substr($hostingId, -2)."/".$hostingId; return substr($hostingId, -2)."/".$hostingId;
} }
} }

View File

@ -5,16 +5,16 @@ require_once 'FileStorageBackend.php';
require_once 'Amazon_S3StorageBackend.php'; require_once 'Amazon_S3StorageBackend.php';
/** /**
* *
* Controls access to the storage backend class where a file is stored. * Controls access to the storage backend class where a file is stored.
* *
*/ */
class ProxyStorageBackend extends StorageBackend class ProxyStorageBackend extends StorageBackend
{ {
private $storageBackend; private $storageBackend;
/** /**
* Receives the file's storage backend and instantiates the approriate * Receives the file's storage backend and instantiates the appropriate
* object. * object.
*/ */
public function ProxyStorageBackend($storageBackend) public function ProxyStorageBackend($storageBackend)
@ -32,17 +32,17 @@ class ProxyStorageBackend extends StorageBackend
$this->storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]); $this->storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]);
} }
} }
public function getAbsoluteFilePath($resourceId) public function getAbsoluteFilePath($resourceId)
{ {
return $this->storageBackend->getAbsoluteFilePath($resourceId); return $this->storageBackend->getAbsoluteFilePath($resourceId);
} }
public function getSignedURL($resourceId) public function getSignedURL($resourceId)
{ {
return $this->storageBackend->getSignedURL($resourceId); return $this->storageBackend->getSignedURL($resourceId);
} }
public function deletePhysicalFile($resourceId) public function deletePhysicalFile($resourceId)
{ {
$this->storageBackend->deletePhysicalFile($resourceId); $this->storageBackend->deletePhysicalFile($resourceId);

View File

@ -14,8 +14,16 @@ class Billing
/** Get the Airtime instance ID of the instance the customer is currently viewing. */ /** Get the Airtime instance ID of the instance the customer is currently viewing. */
public static function getClientInstanceId() public static function getClientInstanceId()
{ {
$currentProduct = Billing::getClientCurrentAirtimeProduct(); //$currentProduct = Billing::getClientCurrentAirtimeProduct();
return $currentProduct["id"]; //return $currentProduct["id"];
//XXX: Major hack attack. Since this function gets called often, rather than querying WHMCS
// we're just going to extract it from airtime.conf since it's the same as the rabbitmq username.
$CC_CONFIG = Config::getConfig();
$instanceId = $CC_CONFIG['rabbitmq']['user'];
if (!is_numeric($instanceId)) {
throw new Exception("Invalid instance id in " . __FUNCTION__ . ": " . $instanceId);
}
return $instanceId;
} }
public static function getProducts() public static function getProducts()
@ -318,4 +326,4 @@ class Billing
$result = Billing::makeRequest($credentials["url"], $query_string); $result = Billing::makeRequest($credentials["url"], $query_string);
} }
} }

View File

@ -24,5 +24,4 @@ class FileDataHelper {
$data["bpm"] = intval($data["bpm"]); $data["bpm"] = intval($data["bpm"]);
} }
} }
} }

View File

@ -0,0 +1,77 @@
<?php
/**
* Class Application_Common_FileIO contains helper functions for reading and writing files, and sending them over HTTP.
*/
class Application_Common_FileIO
{
/**
* Reads the requested portion of a file and sends its contents to the client with the appropriate headers.
*
* 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 $mimeType - the file's mime type. Defaults to 'audio/mp3'
* @param integer $size - the file size, in bytes
* @return void
*
* @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ
* @link http://php.net/manual/en/function.readfile.php#86244
*/
public static function smartReadFile($filePath, $size, $mimeType)
{
$fm = @fopen($filePath, 'rb');
if (!$fm) {
header ("HTTP/1.1 505 Internal server error");
return;
}
//Note that $size is allowed to be zero. If that's the case, it means we don't
//know the filesize, and we just won't send the Content-Length header.
if ($size < 0) {
throw new Exception("Invalid file size returned for file at $filePath");
}
$begin = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
$begin = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
}
}
if (isset($_SERVER['HTTP_RANGE'])) {
header('HTTP/1.1 206 Partial Content');
} else {
header('HTTP/1.1 200 OK');
}
header("Content-Type: $mimeType");
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Accept-Ranges: bytes');
if ($size > 0) {
header('Content-Length:' . (($end - $begin) + 1));
if (isset($_SERVER['HTTP_RANGE'])) {
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());
// NOTE: We can't use fseek here because it does not work with streams
// (a.k.a. Files stored in the cloud)
while(!feof($fm) && (connection_status() == 0)) {
echo fread($fm, 1024 * 8);
}
fclose($fm);
}
}

View File

@ -17,4 +17,28 @@ class Application_Common_HTTPHelper
$request->getParam("timezone", null) $request->getParam("timezone", null)
); );
} }
public static function getStationUrl()
{
$CC_CONFIG = Config::getConfig();
$baseUrl = $CC_CONFIG['baseUrl'];
$baseDir = $CC_CONFIG['baseDir'];
$basePort = $CC_CONFIG['basePort'];
if (empty($baseDir)) {
$baseDir = "/";
}
if ($baseDir[0] != "") {
$baseDir = "/" . $baseDir;
}
$scheme = "http";
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
$scheme = "https";
$basePort = "443"; //Airtime Pro compatibility hack
}
$stationUrl = "$scheme://${baseUrl}:${basePort}${baseDir}";
return $stationUrl;
}
} }

View File

@ -36,19 +36,33 @@ class ProvisioningHelper
$this->parsePostParams(); $this->parsePostParams();
//For security, the Airtime Pro provisioning system creates the database for the user. //For security, the Airtime Pro provisioning system creates the database for the user.
// $this->setNewDatabaseConnection(); $this->setNewDatabaseConnection();
//if ($this->checkDatabaseExists()) { //if ($this->checkDatabaseExists()) {
// throw new Exception("ERROR: Airtime database already exists"); // throw new Exception("ERROR: Airtime database already exists");
//} //}
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");
}
//$this->createDatabase(); //$this->createDatabase();
//All we need to do is create the database tables. //All we need to do is create the database tables.
$this->createDatabaseTables(); $this->createDatabaseTables();
$this->initializeMusicDirsTable($this->instanceId); $this->initializeMusicDirsTable($this->instanceId);
} catch (Exception $e) { } catch (Exception $e) {
http_response_code(400); http_response_code(400);
Logging::error($e->getMessage()); Logging::error($e->getMessage()
echo $e->getMessage(); );
echo $e->getMessage() . PHP_EOL;
return; return;
} }
@ -67,6 +81,19 @@ class ProvisioningHelper
return isset($result[0]); return isset($result[0]);
} }
private function checkTablesExist()
{
try {
$result = self::$dbh->query("SELECT 1 FROM cc_files LIMIT 1");
} catch (Exception $e) {
// We got an exception == table not found
echo($e . PHP_EOL);
return FALSE;
}
// Result is either boolean FALSE (no table found) or PDOStatement Object (table found)
return $result !== FALSE;
}
private function parsePostParams() private function parsePostParams()
{ {
$this->dbuser = $_POST['dbuser']; $this->dbuser = $_POST['dbuser'];
@ -84,9 +111,11 @@ class ProvisioningHelper
private function setNewDatabaseConnection() private function setNewDatabaseConnection()
{ {
self::$dbh = new PDO("pgsql:host=" . $this->dbhost self::$dbh = new PDO("pgsql:host=" . $this->dbhost
. ";dbname=postgres" . ";dbname=" . $this->dbname
. ";port=5432" . ";user=" . $this->dbuser . ";port=5432" . ";user=" . $this->dbuser
. ";password=" . $this->dbpass); . ";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(); $err = self::$dbh->errorInfo();
if ($err[1] != null) { if ($err[1] != null) {
throw new PDOException("ERROR: Could not connect to database"); throw new PDOException("ERROR: Could not connect to database");

View File

@ -43,7 +43,7 @@ class Config {
} else { } else {
$CC_CONFIG['dev_env'] = 'production'; $CC_CONFIG['dev_env'] = 'production';
} }
// Parse separate conf file for cloud storage values // Parse separate conf file for cloud storage values
$cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf"; $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf";
if (!file_exists($cloudStorageConfig)) { if (!file_exists($cloudStorageConfig)) {

View File

@ -95,126 +95,11 @@ class ApiController extends Zend_Controller_Action
$fileId = $this->_getParam("file"); $fileId = $this->_getParam("file");
$media = Application_Model_StoredFile::RecallById($fileId); $inline = !($this->_getParam('download',false) == true);
if ($media != null) { Application_Service_MediaService::streamFileDownload($fileId, $inline);
// Make sure we don't have some wrong result beecause of caching
clearstatcache();
if ($media->getPropelOrm()->isValidPhysicalFile()) {
$filename = $media->getPropelOrm()->getFilename();
//Download user left clicks a track and selects Download.
if ("true" == $this->_getParam('download')) {
//path_info breaks up a file path into seperate pieces of informaiton.
//We just want the basename which is the file name with the path
//information stripped away. We are using Content-Disposition to specify
//to the browser what name the file should be saved as.
header('Content-Disposition: attachment; filename="'.$filename.'"');
} else {
//user clicks play button for track preview
header('Content-Disposition: inline; filename="'.$filename.'"');
}
$this->readStoredFileObject($media);
exit;
} else {
header ("HTTP/1.1 404 Not Found");
}
}
$this->_helper->json->sendJson(array()); $this->_helper->json->sendJson(array());
} }
/**
* Read data from StoredFile object and send with XHR response
*
* @param Application_Model_StoredFile $storedFile - StoredFile object holding file information
*/
private function readStoredFileObject($storedFile) {
$filepath = $storedFile->getFilePath();
$size = $storedFile->getFileSize();
$mimeType = $storedFile->getPropelOrm()->getDbMime();
$this->smartReadFile($filepath, $mimeType, $size);
}
/**
* Reads the requested portion of a file and sends its contents to the client with the appropriate headers.
*
* This HTTP_RANGE compatible read file function is necessary for allowing streaming media to be skipped around in.
*
* @param string $location - the full filepath 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
*
* @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ
* @link http://php.net/manual/en/function.readfile.php#86244
*/
private function smartReadFile($location, $mimeType = 'audio/mp3', $size = null)
{
if (!$location || $location == "") {
throw new FileDoesNotExistException("Requested file does not exist!");
}
// If we're passing in a Stored File object, it's faster
// to use getFileSize() and pass in the result
if (!isset($size) || $size < 0) {
$size= filesize($location);
}
if ($size < 0) {
throw new Exception("Invalid file size returned for file at $location");
}
$fm = @fopen($location, 'rb');
if (!$fm) {
header ("HTTP/1.1 505 Internal server error");
return;
}
$begin = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
$begin = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
}
}
if (isset($_SERVER['HTTP_RANGE'])) {
header('HTTP/1.1 206 Partial Content');
} else {
header('HTTP/1.1 200 OK');
}
header("Content-Type: $mimeType");
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Accept-Ranges: bytes');
if ($size > 0) {
header('Content-Length:' . (($end - $begin) + 1));
if (isset($_SERVER['HTTP_RANGE'])) {
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());
// NOTE: We can't use fseek here because it does not work with streams
// (a.k.a. Files stored in the cloud)
while(!feof($fm) && (connection_status() == 0)) {
echo fread($fm, 1024 * 8);
}
fclose($fm);
}
//Used by the SaaS monitoring //Used by the SaaS monitoring
public function onAirLightAction() public function onAirLightAction()
@ -610,9 +495,8 @@ class ApiController extends Zend_Controller_Action
$path = $show->getDbImagePath(); $path = $show->getDbImagePath();
$mime_type = mime_content_type($path); $mime_type = mime_content_type($path);
header("Content-type: " . $mime_type); Application_Common_FileIO::smartReadFile($path, filesize($path), $mime_type);
$this->smartReadFile($path, $mime_type);
} else { } else {
header('HTTP/1.0 401 Unauthorized'); header('HTTP/1.0 401 Unauthorized');
print _('You are not allowed to access this resource. '); print _('You are not allowed to access this resource. ');

View File

@ -22,8 +22,6 @@ class AudiopreviewController extends Zend_Controller_Action
$CC_CONFIG = Config::getConfig(); $CC_CONFIG = Config::getConfig();
$audioFileID = $this->_getParam('audioFileID'); $audioFileID = $this->_getParam('audioFileID');
$audioFileArtist = $this->_getParam('audioFileArtist');
$audioFileTitle = $this->_getParam('audioFileTitle');
$type = $this->_getParam('type'); $type = $this->_getParam('type');
$baseUrl = Application_Common_OsPath::getBaseDir(); $baseUrl = Application_Common_OsPath::getBaseDir();
@ -49,10 +47,15 @@ class AudiopreviewController extends Zend_Controller_Action
$media = Application_Model_StoredFile::RecallById($audioFileID); $media = Application_Model_StoredFile::RecallById($audioFileID);
$uri = $baseUrl."api/get-media/file/".$audioFileID; $uri = $baseUrl."api/get-media/file/".$audioFileID;
$mime = $media->getPropelOrm()->getDbMime(); $mime = $media->getPropelOrm()->getDbMime();
$this->view->audioFileArtist = htmlspecialchars($media->getPropelOrm()->getDbArtistName());
$this->view->audioFileTitle = htmlspecialchars($media->getPropelOrm()->getDbTrackTitle());
} elseif ($type == "stream") { } elseif ($type == "stream") {
$webstream = CcWebstreamQuery::create()->findPk($audioFileID); $webstream = CcWebstreamQuery::create()->findPk($audioFileID);
$uri = $webstream->getDbUrl(); $uri = $webstream->getDbUrl();
$mime = $webstream->getDbMime(); $mime = $webstream->getDbMime();
$this->view->audioFileTitle = htmlspecialchars($webstream->getDbName());
} else { } else {
throw new Exception("Unknown type for audio preview!.Type=$type"); throw new Exception("Unknown type for audio preview!.Type=$type");
} }
@ -60,10 +63,7 @@ class AudiopreviewController extends Zend_Controller_Action
$this->view->uri = $uri; $this->view->uri = $uri;
$this->view->mime = $mime; $this->view->mime = $mime;
$this->view->audioFileID = $audioFileID; $this->view->audioFileID = $audioFileID;
// We need to decode artist and title because it gets
// encoded twice in js
$this->view->audioFileArtist = htmlspecialchars(urldecode($audioFileArtist));
$this->view->audioFileTitle = htmlspecialchars(urldecode($audioFileTitle));
$this->view->type = $type; $this->view->type = $type;
$this->_helper->viewRenderer->setRender('audio-preview'); $this->_helper->viewRenderer->setRender('audio-preview');

View File

@ -217,7 +217,7 @@ class LibraryController extends Zend_Controller_Action
// and not the cloud_file id (if applicable) for track download. // and not the cloud_file id (if applicable) for track download.
// Our application logic (StoredFile.php) will determine if the track // Our application logic (StoredFile.php) will determine if the track
// is a cloud_file and handle it appropriately. // is a cloud_file and handle it appropriately.
$url = $baseUrl."api/get-media/file/".$id.".".$file->getFileExtension().'/download/true'; $url = $baseUrl."api/get-media/file/$id/download/true";
$menu["download"] = array("name" => _("Download"), "icon" => "download", "url" => $url); $menu["download"] = array("name" => _("Download"), "icon" => "download", "url" => $url);
} elseif ($type === "playlist" || $type === "block") { } elseif ($type === "playlist" || $type === "block") {
if ($type === 'playlist') { if ($type === 'playlist') {

View File

@ -119,6 +119,9 @@ class LoginController extends Zend_Controller_Action
{ {
$auth = Zend_Auth::getInstance(); $auth = Zend_Auth::getInstance();
$auth->clearIdentity(); $auth->clearIdentity();
// Unset all session variables relating to CSRF prevention on logout
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
$csrf_namespace->unsetAll();
$this->_redirect('showbuilder/index'); $this->_redirect('showbuilder/index');
} }

View File

@ -31,9 +31,10 @@ class PluploadController extends Zend_Controller_Action
$this->view->quotaLimitReached = true; $this->view->quotaLimitReached = true;
} }
//Because uploads are done via AJAX (and we're not using Zend form for those), we manually add the CSRF
//token in here.
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); $csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
$csrf_namespace->setExpirationSeconds(5*60*60); //The CSRF token is generated in Bootstrap.php
$csrf_namespace->authtoken = sha1(uniqid(rand(),1));
$csrf_element = new Zend_Form_Element_Hidden('csrf'); $csrf_element = new Zend_Form_Element_Hidden('csrf');
$csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label');

View File

@ -38,6 +38,7 @@ class PreferenceController extends Zend_Controller_Action
if ($form->isValid($values)) if ($form->isValid($values))
{ {
Application_Model_Preference::SetHeadTitle($values["stationName"], $this->view); Application_Model_Preference::SetHeadTitle($values["stationName"], $this->view);
Application_Model_Preference::SetStationDescription($values["stationDescription"]);
Application_Model_Preference::SetDefaultCrossfadeDuration($values["stationDefaultCrossfadeDuration"]); Application_Model_Preference::SetDefaultCrossfadeDuration($values["stationDefaultCrossfadeDuration"]);
Application_Model_Preference::SetDefaultFadeIn($values["stationDefaultFadeIn"]); Application_Model_Preference::SetDefaultFadeIn($values["stationDefaultFadeIn"]);
Application_Model_Preference::SetDefaultFadeOut($values["stationDefaultFadeOut"]); Application_Model_Preference::SetDefaultFadeOut($values["stationDefaultFadeOut"]);
@ -49,7 +50,11 @@ class PreferenceController extends Zend_Controller_Action
$logoUploadElement = $form->getSubForm('preferences_general')->getElement('stationLogo'); $logoUploadElement = $form->getSubForm('preferences_general')->getElement('stationLogo');
$logoUploadElement->receive(); $logoUploadElement->receive();
$imagePath = $logoUploadElement->getFileName(); $imagePath = $logoUploadElement->getFileName();
Application_Model_Preference::SetStationLogo($imagePath);
// Only update the image logo if the new logo is non-empty
if (!empty($imagePath) && $imagePath != "") {
Application_Model_Preference::SetStationLogo($imagePath);
}
Application_Model_Preference::SetUploadToSoundcloudOption($values["UploadToSoundcloudOption"]); Application_Model_Preference::SetUploadToSoundcloudOption($values["UploadToSoundcloudOption"]);
Application_Model_Preference::SetSoundCloudDownloadbleOption($values["SoundCloudDownloadbleOption"]); Application_Model_Preference::SetSoundCloudDownloadbleOption($values["SoundCloudDownloadbleOption"]);

View File

@ -19,6 +19,7 @@ class UpgradeController extends Zend_Controller_Action
array_push($upgraders, new AirtimeUpgrader255()); array_push($upgraders, new AirtimeUpgrader255());
array_push($upgraders, new AirtimeUpgrader259()); array_push($upgraders, new AirtimeUpgrader259());
array_push($upgraders, new AirtimeUpgrader2510()); array_push($upgraders, new AirtimeUpgrader2510());
array_push($upgraders, new AirtimeUpgrader2511());
$didWePerformAnUpgrade = false; $didWePerformAnUpgrade = false;
try try

View File

@ -152,17 +152,22 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
} }
} }
} else { //We have a session/identity. } else { //We have a session/identity.
// If we have an identity and we're making a RESTful request, // If we have an identity and we're making a RESTful request,
// we need to check the CSRF token // we need to check the CSRF token
if ($request->_action != "get" && $request->getModuleName() == "rest") { if ($_SERVER['REQUEST_METHOD'] != "GET" && $request->getModuleName() == "rest") {
$tokenValid = $this->verifyCSRFToken($request->getParam("csrf_token")); $token = $request->getParam("csrf_token");
$tokenValid = $this->verifyCSRFToken($token);
if (!$tokenValid) { if (!$tokenValid) {
$csrf_namespace = new Zend_Session_Namespace('csrf_namespace');
$csrf_namespace->authtoken = sha1(openssl_random_pseudo_bytes(128));
Logging::warn("Invalid CSRF token: $token");
$this->getResponse() $this->getResponse()
->setHttpResponseCode(401) ->setHttpResponseCode(401)
->appendBody("ERROR: CSRF token mismatch."); ->appendBody("ERROR: CSRF token mismatch.")
return; ->sendResponse();
die();
} }
} }
@ -207,7 +212,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
$current_namespace = new Zend_Session_Namespace('csrf_namespace'); $current_namespace = new Zend_Session_Namespace('csrf_namespace');
$observed_csrf_token = $token; $observed_csrf_token = $token;
$expected_csrf_token = $current_namespace->authtoken; $expected_csrf_token = $current_namespace->authtoken;
return ($observed_csrf_token == $expected_csrf_token); return ($observed_csrf_token == $expected_csrf_token);
} }

View File

@ -79,7 +79,7 @@ class Application_Form_AddShowStyle extends Zend_Form_SubForm
->addValidator('Count', false, 1) ->addValidator('Count', false, 1)
->addValidator('Extension', false, 'jpg,jpeg,png,gif') ->addValidator('Extension', false, 'jpg,jpeg,png,gif')
->addFilter('ImageSize'); ->addFilter('ImageSize');
$this->addElement($upload); $this->addElement($upload);
// Add image preview // Add image preview
@ -93,6 +93,14 @@ class Application_Form_AddShowStyle extends Zend_Form_SubForm
'class' => 'big' 'class' => 'big'
)))); ))));
$preview->setAttrib('disabled','disabled'); $preview->setAttrib('disabled','disabled');
$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');
$this->addElement($csrf_element);
} }
public function disable() public function disable()

View File

@ -49,6 +49,13 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
$stationLogoUpload->setAttrib('accept', 'image/*'); $stationLogoUpload->setAttrib('accept', 'image/*');
$this->addElement($stationLogoUpload); $this->addElement($stationLogoUpload);
$stationLogoRemove = new Zend_Form_Element_Button('stationLogoRemove');
$stationLogoRemove->setLabel(_('Remove'));
$stationLogoRemove->setAttrib('class', 'btn');
$stationLogoRemove->setAttrib('id', 'logo-remove-btn');
$stationLogoRemove->setAttrib('onclick', 'removeLogo();');
$this->addElement($stationLogoRemove);
//Default station crossfade duration //Default station crossfade duration
$this->addElement('text', 'stationDefaultCrossfadeDuration', array( $this->addElement('text', 'stationDefaultCrossfadeDuration', array(
'class' => 'input_text', 'class' => 'input_text',

View File

@ -80,7 +80,7 @@ class Application_Model_RabbitMq
} }
public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename, public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename,
$callbackUrl, $apiKey, $currentStorageBackend, $filePrefix) $callbackUrl, $apiKey, $storageBackend, $filePrefix)
{ {
//Hack for Airtime Pro. The RabbitMQ settings for communicating with airtime_analyzer are global //Hack for Airtime Pro. The RabbitMQ settings for communicating with airtime_analyzer are global
//and shared between all instances on Airtime Pro. //and shared between all instances on Airtime Pro.
@ -107,7 +107,7 @@ class Application_Model_RabbitMq
$queue = 'airtime-uploads'; $queue = 'airtime-uploads';
$autoDeleteExchange = false; $autoDeleteExchange = false;
$data['tmp_file_path'] = $tmpFilePath; $data['tmp_file_path'] = $tmpFilePath;
$data['current_storage_backend'] = $currentStorageBackend; $data['storage_backend'] = $storageBackend;
$data['import_directory'] = $importedStorageDirectory; $data['import_directory'] = $importedStorageDirectory;
$data['original_filename'] = $originalFilename; $data['original_filename'] = $originalFilename;
$data['callback_url'] = $callbackUrl; $data['callback_url'] = $callbackUrl;

View File

@ -954,7 +954,6 @@ SQL;
$baseUrl = Application_Common_OsPath::getBaseDir(); $baseUrl = Application_Common_OsPath::getBaseDir();
$filesize = $file->getFileSize(); $filesize = $file->getFileSize();
self::createFileScheduleEvent($data, $item, $media_id, $uri, $filesize); self::createFileScheduleEvent($data, $item, $media_id, $uri, $filesize);
} }

View File

@ -392,13 +392,15 @@ SQL;
Logging::info("User ".$user->getLogin()." is deleting file: ".$this->_file->getDbTrackTitle()." - file id: ".$file_id); Logging::info("User ".$user->getLogin()." is deleting file: ".$this->_file->getDbTrackTitle()." - file id: ".$file_id);
$filesize = $this->_file->getFileSize(); $filesize = $this->_file->getFileSize();
if ($filesize <= 0) { if ($filesize < 0) {
throw new Exception("Cannot delete file with filesize ".$filesize); throw new Exception("Cannot delete file with filesize ".$filesize);
} }
//Delete the physical file from either the local stor directory //Delete the physical file from either the local stor directory
//or from the cloud //or from the cloud
$this->_file->deletePhysicalFile(); if ($this->_file->getDbImportStatus() == CcFiles::IMPORT_STATUS_SUCCESS) {
$this->_file->deletePhysicalFile();
}
//Update the user's disk usage //Update the user's disk usage
Application_Model_Preference::updateDiskUsage(-1 * $filesize); Application_Model_Preference::updateDiskUsage(-1 * $filesize);
@ -944,19 +946,17 @@ SQL;
* The file is actually copied to "stor/organize", which is a staging directory where files go * The file is actually copied to "stor/organize", which is a staging directory where files go
* before they're processed by airtime_analyzer, which then moves them to "stor/imported" in the final * before they're processed by airtime_analyzer, which then moves them to "stor/imported" in the final
* step. * step.
*
* TODO: Implement better error handling here...
* *
* @param string $tempFilePath * @param string $tempFilePath
* @param string $originalFilename * @param string $originalFilename
* @param bool $copyFile Copy the file instead of moving it.
* @throws Exception * @throws Exception
* @return Ambigous <unknown, string> * @return Ambigous <unknown, string>
*/ */
public static function copyFileToStor($tempFilePath, $originalFilename) public static function moveFileToStor($tempFilePath, $originalFilename, $copyFile=false)
{ {
$audio_file = $tempFilePath; $audio_file = $tempFilePath;
Logging::info('copyFileToStor: moving file '.$audio_file);
$storDir = Application_Model_MusicDir::getStorDir(); $storDir = Application_Model_MusicDir::getStorDir();
$stor = $storDir->getDirectory(); $stor = $storDir->getDirectory();
// check if "organize" dir exists and if not create one // check if "organize" dir exists and if not create one
@ -970,57 +970,34 @@ SQL;
Logging::info("Warning: couldn't change permissions of $audio_file to 0644"); Logging::info("Warning: couldn't change permissions of $audio_file to 0644");
} }
// Check if liquidsoap can play this file
// TODO: Move this to airtime_analyzer
/*
if (!self::liquidsoapFilePlayabilityTest($audio_file)) {
return array(
"code" => 110,
"message" => _("This file appears to be corrupted and will not "
."be added to media library."));
}*/
// Did all the checks for real, now trying to copy // Did all the checks for real, now trying to copy
$audio_stor = Application_Common_OsPath::join($stor, "organize", $audio_stor = Application_Common_OsPath::join($stor, "organize",
$originalFilename); $originalFilename);
Logging::info($originalFilename);
Logging::info($audio_stor);
$user = Application_Model_User::getCurrentUser();
if (is_null($user)) {
$uid = Application_Model_User::getFirstAdminId();
} else {
$uid = $user->getId();
}
/*
$id_file = "$audio_stor.identifier";
if (file_put_contents($id_file, $uid) === false) {
Logging::info("Could not write file to identify user: '$uid'");
Logging::info("Id file path: '$id_file'");
Logging::info("Defaulting to admin (no identification file was
written)");
} else {
Logging::info("Successfully written identification file for
uploaded '$audio_stor'");
}*/
//if the uploaded file is not UTF-8 encoded, let's encode it. Assuming source //if the uploaded file is not UTF-8 encoded, let's encode it. Assuming source
//encoding is ISO-8859-1 //encoding is ISO-8859-1
$audio_stor = mb_detect_encoding($audio_stor, "UTF-8") == "UTF-8" ? $audio_stor : utf8_encode($audio_stor); $audio_stor = mb_detect_encoding($audio_stor, "UTF-8") == "UTF-8" ? $audio_stor : utf8_encode($audio_stor);
Logging::info("copyFileToStor: moving file $audio_file to $audio_stor"); if ($copyFile) {
// Martin K.: changed to rename: Much less load + quicker since this is Logging::info("Copying file $audio_file to $audio_stor");
// an atomic operation if (@copy($audio_file, $audio_stor) === false) {
if (@rename($audio_file, $audio_stor) === false) { throw new Exception("Failed to copy $audio_file to $audio_stor");
//something went wrong likely there wasn't enough space in . }
//the audio_stor to move the file too warn the user that . } else {
//the file wasn't uploaded and they should check if there . Logging::info("Moving file $audio_file to $audio_stor");
//is enough disk space .
unlink($audio_file); //remove the file after failed rename // Martin K.: changed to rename: Much less load + quicker since this is
//unlink($id_file); // Also remove the identifier file // an atomic operation
if (@rename($audio_file, $audio_stor) === false) {
throw new Exception("The file was not uploaded, this error can occur if the computer " //something went wrong likely there wasn't enough space in .
."hard drive does not have enough disk space or the stor " //the audio_stor to move the file too warn the user that .
."directory does not have correct write permissions."); //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 "
. "directory does not have correct write permissions.");
}
} }
return $audio_stor; return $audio_stor;
} }

View File

@ -11,17 +11,313 @@
* *
* @package propel.generator.campcaster * @package propel.generator.campcaster
*/ */
class InvalidMetadataException extends Exception
{
}
class FileNotFoundException extends Exception
{
}
class OverDiskQuotaException extends Exception
{
}
class CcFiles extends BaseCcFiles { class CcFiles extends BaseCcFiles {
const MUSIC_DIRS_STOR_PK = 1;
const IMPORT_STATUS_SUCCESS = 0;
const IMPORT_STATUS_PENDING = 1;
const IMPORT_STATUS_FAILED = 2;
//fields that are not modifiable via our RESTful API
private static $blackList = array(
'id',
'directory',
'filepath',
'file_exists',
'mtime',
'utime',
'lptime',
'silan_check',
'soundcloud_id',
'is_scheduled',
'is_playlist'
);
//fields we should never expose through our RESTful API //fields we should never expose through our RESTful API
private static $privateFields = array( private static $privateFields = array(
'file_exists', 'file_exists',
'silan_check', 'silan_check',
'is_scheduled', 'is_scheduled',
'is_playlist' 'is_playlist'
); );
public function getCueLength() /**
* Retrieve a sanitized version of the file metadata, suitable for public access.
* @param $fileId
*/
public static function getSanitizedFileById($fileId)
{
$file = CcFilesQuery::create()->findPk($fileId);
if ($file) {
return CcFiles::sanitizeResponse($file);
} else {
throw new FileNotFoundException();
}
}
/** Used to create a CcFiles object from an array containing metadata and a file uploaded by POST.
* This is used by our Media REST API!
* @param $fileArray An array containing metadata for a CcFiles object.
* @throws Exception
*/
public static function createFromUpload($fileArray)
{
if (Application_Model_Systemstatus::isDiskOverQuota()) {
throw new OverDiskQuotaException();
}
/* If full_path is set, the post request came from ftp.
* Users are allowed to upload folders via ftp. If this is the case
* we need to include the folder name with the file name, otherwise
* files won't get removed from the organize folder.
*/
//Extract the original filename, which we set as the temporary title for the track
//until it's finished being processed by the analyzer.
$originalFilename = $_FILES["file"]["name"];
$tempFilePath = $_FILES['file']['tmp_name'];
try {
self::createAndImport($fileArray, $tempFilePath, $originalFilename);
} catch (Exception $e)
{
@unlink($tempFilePath);
throw $e;
}
}
/** Import a music file to the library from a local file on disk (something pre-existing).
* This function allows you to copy a file rather than move it, which is useful for importing
* static music files (like sample tracks).
* @param string $filePath The full path to the audio file to import.
* @param bool $copyFile True if you want to just copy the false, false if you want to move it (default false)
* @throws Exception
*/
public static function createFromLocalFile($fileArray, $filePath, $copyFile=false)
{
$info = pathinfo($filePath);
$fileName = basename($filePath).'.'.$info['extension'];
self::createAndImport($fileArray, $filePath, $fileName, $copyFile);
}
/** Create a new CcFiles object/row and import a file for it.
* You shouldn't call this directly. Either use createFromUpload() or createFromLocalFile().
* @param array $fileArray Any metadata to pre-fill for the audio file
* @param string $filePath The full path to the audio file to import
* @param string $originalFilename
* @param bool $copyFile
* @return mixed
* @throws Exception
* @throws PropelException
*/
private static function createAndImport($fileArray, $filePath, $originalFilename, $copyFile=false)
{
$file = new CcFiles();
try
{
$fileArray = self::removeBlacklistedFields($fileArray);
self::validateFileArray($fileArray);
$file->fromArray($fileArray);
$file->setDbOwnerId(self::getOwnerId());
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbTrackTitle($originalFilename);
$file->setDbUtime($now);
$file->setDbHidden(true);
$file->save();
//Only accept files with a file extension that we support.
$fileExtension = pathinfo($originalFilename, PATHINFO_EXTENSION);
if (!in_array(strtolower($fileExtension), explode(",", "ogg,mp3,oga,flac,wav,m4a,mp4,opus"))) {
throw new Exception("Bad file extension.");
}
$callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "/rest/media/" . $file->getPrimaryKey();
Application_Service_MediaService::importFileToLibrary($callbackUrl, $filePath,
$originalFilename, self::getOwnerId(), $copyFile);
return CcFiles::sanitizeResponse($file);
} catch (Exception $e) {
$file->setDbImportStatus(self::IMPORT_STATUS_FAILED);
$file->setDbHidden(true);
$file->save();
throw $e;
}
}
/** Update a file with metadata specified in an array.
* @param $fileId string The ID of the file to update in the DB.
* @param $fileArray array An associative array containing metadata. Replaces those fields if they exist.
* @return array A sanitized version of the file metadata array.
* @throws Exception
* @throws FileNotFoundException
* @throws PropelException
*/
public static function updateFromArray($fileId, $fileArray)
{
$file = CcFilesQuery::create()->findPk($fileId);
$fileArray = self::removeBlacklistedFields($fileArray);
$fileArray = self::stripTimeStampFromYearTag($fileArray);
try {
self::validateFileArray($fileArray);
if ($file && isset($fileArray["resource_id"])) {
$file->fromArray($fileArray, BasePeer::TYPE_FIELDNAME);
//store the original filename
$file->setDbFilepath($fileArray["filename"]);
$fileSizeBytes = $fileArray["filesize"];
if (!isset($fileSizeBytes) || $fileSizeBytes === false) {
throw new FileNotFoundException("Invalid filesize for $fileId");
}
$cloudFile = new CloudFile();
$cloudFile->setStorageBackend($fileArray["storage_backend"]);
$cloudFile->setResourceId($fileArray["resource_id"]);
$cloudFile->setCcFiles($file);
$cloudFile->save();
Application_Model_Preference::updateDiskUsage($fileSizeBytes);
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbMtime($now);
$file->save();
} else if ($file) {
// Since we check for this value when deleting files, set it first
$file->setDbDirectory(self::MUSIC_DIRS_STOR_PK);
$file->fromArray($fileArray, BasePeer::TYPE_FIELDNAME);
//Our RESTful API takes "full_path" as a field, which we then split and translate to match
//our internal schema. Internally, file path is stored relative to a directory, with the directory
//as a foreign key to cc_music_dirs.
if (isset($fileArray["full_path"])) {
$fileSizeBytes = filesize($fileArray["full_path"]);
if (!isset($fileSizeBytes) || $fileSizeBytes === false) {
throw new FileNotFoundException("Invalid filesize for $fileId");
}
Application_Model_Preference::updateDiskUsage($fileSizeBytes);
$fullPath = $fileArray["full_path"];
$storDir = Application_Model_MusicDir::getStorDir()->getDirectory();
$pos = strpos($fullPath, $storDir);
if ($pos !== FALSE) {
assert($pos == 0); //Path must start with the stor directory path
$filePathRelativeToStor = substr($fullPath, strlen($storDir));
$file->setDbFilepath($filePathRelativeToStor);
}
}
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbMtime($now);
$file->save();
} else {
throw new FileNotFoundException();
}
}
catch (FileNotFoundException $e)
{
$file->setDbImportStatus(self::IMPORT_STATUS_FAILED);
$file->setDbHidden(true);
$file->save();
throw $e;
}
return CcFiles::sanitizeResponse($file);
}
/** Delete a file from the database and disk (or cloud).
* @param $id The file ID
* @throws DeleteScheduledFileException
* @throws Exception
* @throws FileNoPermissionException
* @throws FileNotFoundException
* @throws PropelException
*/
public static function deleteById($id)
{
$file = CcFilesQuery::create()->findPk($id);
if ($file) {
$con = Propel::getConnection();
$storedFile = Application_Model_StoredFile::RecallById($id, $con);
$storedFile->delete();
} else {
throw new FileNotFoundException();
}
}
private static function validateFileArray(&$fileArray)
{
// Sanitize any wildly incorrect metadata before it goes to be validated
FileDataHelper::sanitizeData($fileArray);
// EditAudioMD form is used here for validation
$fileForm = new Application_Form_EditAudioMD();
$fileForm->startForm(0); //The file ID doesn't matter here
$fileForm->populate($fileArray);
/*
* Here we are truncating metadata of any characters greater than the
* max string length set in the database. In the rare case a track's
* genre is more than 64 chars, for example, we don't want to reject
* tracks for that reason
*/
foreach($fileArray as $tag => &$value) {
if ($fileForm->getElement($tag)) {
$stringLengthValidator = $fileForm->getElement($tag)->getValidator('StringLength');
//$stringLengthValidator will be false if the StringLength validator doesn't exist on the current element
//in which case we don't have to truncate the extra characters
if ($stringLengthValidator) {
$value = substr($value, 0, $stringLengthValidator->getMax());
}
$value = self::stripInvalidUtf8Characters($value);
}
}
if (!$fileForm->isValidPartial($fileArray)) {
$errors = $fileForm->getErrors();
$messages = $fileForm->getMessages();
Logging::error($messages);
throw new Exception("Data validation failed: $errors - $messages");
}
return true;
}
public function getCueLength()
{ {
$cuein = $this->getDbCuein(); $cuein = $this->getDbCuein();
$cueout = $this->getDbCueout(); $cueout = $this->getDbCueout();
@ -57,7 +353,7 @@ class CcFiles extends BaseCcFiles {
/** /**
* *
* Strips out the private fields we do not want to send back in API responses * Strips out the private fields we do not want to send back in API responses
* @param $file a CcFiles object * @param $file string a CcFiles object
*/ */
//TODO: rename this function? //TODO: rename this function?
public static function sanitizeResponse($file) public static function sanitizeResponse($file)
@ -70,7 +366,7 @@ class CcFiles extends BaseCcFiles {
return $response; return $response;
} }
/** /**
* Returns the file size in bytes. * Returns the file size in bytes.
*/ */
@ -78,13 +374,14 @@ class CcFiles extends BaseCcFiles {
{ {
return $this->getDbFilesize(); return $this->getDbFilesize();
} }
public function getFilename() public function getFilename()
{ {
$info = pathinfo($this->getAbsoluteFilePath()); $info = pathinfo($this->getAbsoluteFilePath());
return $info['filename']; //filename doesn't contain the extension because PHP is awful
return $info['filename'].".".$info['extension'];
} }
/** /**
* Returns the file's absolute file path stored on disk. * Returns the file's absolute file path stored on disk.
*/ */
@ -92,7 +389,7 @@ class CcFiles extends BaseCcFiles {
{ {
return $this->getAbsoluteFilePath(); return $this->getAbsoluteFilePath();
} }
/** /**
* Returns the file's absolute file path stored on disk. * Returns the file's absolute file path stored on disk.
*/ */
@ -100,14 +397,93 @@ class CcFiles extends BaseCcFiles {
{ {
$music_dir = Application_Model_MusicDir::getDirByPK($this->getDbDirectory()); $music_dir = Application_Model_MusicDir::getDirByPK($this->getDbDirectory());
if (!$music_dir) { if (!$music_dir) {
throw new Exception("Invalid music_dir for file in database."); throw new Exception("Invalid music_dir for file " . $this->getDbId() . " in database.");
} }
$directory = $music_dir->getDirectory(); $directory = $music_dir->getDirectory();
$filepath = $this->getDbFilepath(); $filepath = $this->getDbFilepath();
return Application_Common_OsPath::join($directory, $filepath); return Application_Common_OsPath::join($directory, $filepath);
} }
/**
*
* Strips out fields from incoming request data that should never be modified
* from outside of Airtime
* @param array $data
*/
private static function removeBlacklistedFields($data)
{
foreach (self::$blackList as $key) {
unset($data[$key]);
}
return $data;
}
private static function getOwnerId()
{
try {
if (Zend_Auth::getInstance()->hasIdentity()) {
$service_user = new Application_Service_UserService();
return $service_user->getCurrentUser()->getDbId();
} else {
$defaultOwner = CcSubjsQuery::create()
->filterByDbType('A')
->orderByDbId()
->findOne();
if (!$defaultOwner) {
// what to do if there is no admin user?
// should we handle this case?
return null;
}
return $defaultOwner->getDbId();
}
} catch(Exception $e) {
Logging::info($e->getMessage());
}
}
/*
* It's possible that the year tag will be a timestamp but Airtime doesn't support this.
* The year field in cc_files can only be 16 chars max.
*
* This functions strips the year field of it's timestamp, if one, and leaves just the year
*/
private static function stripTimeStampFromYearTag($metadata)
{
if (isset($metadata["year"])) {
if (preg_match("/^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/", $metadata["year"])) {
$metadata["year"] = substr($metadata["year"], 0, 4);
}
}
return $metadata;
}
private static function stripInvalidUtf8Characters($string)
{
//Remove invalid UTF-8 characters
//reject overly long 2 byte sequences, as well as characters above U+10000 and replace with ?
$string = preg_replace('/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]'.
'|[\x00-\x7F][\x80-\xBF]+'.
'|([\xC0\xC1]|[\xF0-\xFF])[\x80-\xBF]*'.
'|[\xC2-\xDF]((?![\x80-\xBF])|[\x80-\xBF]{2,})'.
'|[\xE0-\xEF](([\x80-\xBF](?![\x80-\xBF]))|(?![\x80-\xBF]{2})|[\x80-\xBF]{3,})/S',
'?', $string );
//reject overly long 3 byte sequences and UTF-16 surrogates and replace with ?
$string = preg_replace('/\xE0[\x80-\x9F][\x80-\xBF]'.
'|\xED[\xA0-\xBF][\x80-\xBF]/S','?', $string );
//Do a final encoding conversion to
$string = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
return $string;
}
private function removeEmptySubFolders($path)
{
exec("find $path -empty -type d -delete");
}
/** /**
* Checks if the file is a regular file that can be previewed and downloaded. * Checks if the file is a regular file that can be previewed and downloaded.
*/ */

View File

@ -5,6 +5,9 @@ require_once 'ProxyStorageBackend.php';
/** /**
* Skeleton subclass for representing a row from the 'cloud_file' table. * Skeleton subclass for representing a row from the 'cloud_file' table.
* *
* This class uses Propel's delegation feature to virtually inherit from CcFile!
* You can call any CcFile method on this function and it will work! -- Albert
*
* Each cloud_file has a corresponding cc_file referenced as a foreign key. * Each cloud_file has a corresponding cc_file referenced as a foreign key.
* The file's metadata is stored in the cc_file table. This, cloud_file, * The file's metadata is stored in the cc_file table. This, cloud_file,
* table represents files that are stored in the cloud. * table represents files that are stored in the cloud.

View File

@ -4,27 +4,6 @@ require_once 'ProxyStorageBackend.php';
class Rest_MediaController extends Zend_Rest_Controller class Rest_MediaController extends Zend_Rest_Controller
{ {
const MUSIC_DIRS_STOR_PK = 1;
const IMPORT_STATUS_SUCCESS = 0;
const IMPORT_STATUS_PENDING = 1;
const IMPORT_STATUS_FAILED = 2;
//fields that are not modifiable via our RESTful API
private static $blackList = array(
'id',
'directory',
'filepath',
'file_exists',
'mtime',
'utime',
'lptime',
'silan_check',
'soundcloud_id',
'is_scheduled',
'is_playlist'
);
public function init() public function init()
{ {
$this->view->layout()->disableLayout(); $this->view->layout()->disableLayout();
@ -35,15 +14,33 @@ class Rest_MediaController extends Zend_Rest_Controller
public function indexAction() public function indexAction()
{ {
$totalFileCount = CcFilesQuery::create()->count();
// Check if offset and limit were sent with request.
// Default limit to zero and offset to $totalFileCount
$offset = $this->_getParam('offset', 0);
$limit = $this->_getParam('limit', $totalFileCount);
$query = CcFilesQuery::create()
->filterByDbHidden(false)
->filterByDbFileExists(true)
->filterByDbImportStatus(0)
->setLimit($limit)
->setOffset($offset)
->orderByDbId();
$queryCount = $query->count();
$queryResult = $query->find();
$files_array = array(); $files_array = array();
foreach (CcFilesQuery::create()->find() as $file) foreach ($queryResult as $file)
{ {
array_push($files_array, CcFiles::sanitizeResponse($file)); array_push($files_array, CcFiles::sanitizeResponse($file));
} }
$this->getResponse() $this->getResponse()
->setHttpResponseCode(200) ->setHttpResponseCode(200)
->appendBody(json_encode($files_array)); ->setHeader('X-TOTAL-COUNT', $queryCount)
->appendBody(json_encode($files_array));
/** TODO: Use this simpler code instead after we upgrade to Propel 1.7 (Airtime 2.6.x branch): /** TODO: Use this simpler code instead after we upgrade to Propel 1.7 (Airtime 2.6.x branch):
$this->getResponse() $this->getResponse()
@ -59,18 +56,20 @@ class Rest_MediaController extends Zend_Rest_Controller
return; return;
} }
$file = CcFilesQuery::create()->findPk($id); try
if ($file) { {
$con = Propel::getConnection();
$storedFile = new Application_Model_StoredFile($file, $con);
$baseUrl = Application_Common_OsPath::getBaseDir();
$CC_CONFIG = Config::getConfig();
$this->getResponse() $this->getResponse()
->setHttpResponseCode(200) ->setHttpResponseCode(200);
->appendBody($this->_redirect($storedFile->getRelativeFileUrl($baseUrl).'/download/true/api_key/'.$CC_CONFIG["apiKey"][0])); $inline = false;
} else { Application_Service_MediaService::streamFileDownload($id, $inline);
}
catch (FileNotFoundException $e) {
$this->fileNotFoundResponse(); $this->fileNotFoundResponse();
Logging::error($e->getMessage());
}
catch (Exception $e) {
$this->unknownErrorResponse();
Logging::error($e->getMessage());
} }
} }
@ -81,14 +80,18 @@ class Rest_MediaController extends Zend_Rest_Controller
return; return;
} }
$file = CcFilesQuery::create()->findPk($id); try {
if ($file) {
$this->getResponse() $this->getResponse()
->setHttpResponseCode(200) ->setHttpResponseCode(200)
->appendBody(json_encode(CcFiles::sanitizeResponse($file))); ->appendBody(json_encode(CcFiles::getSanitizedFileById($id)));
} else { }
catch (FileNotFoundException $e) {
$this->fileNotFoundResponse(); $this->fileNotFoundResponse();
Logging::error($e->getMessage());
}
catch (Exception $e) {
$this->unknownErrorResponse();
Logging::error($e->getMessage());
} }
} }
@ -103,52 +106,24 @@ class Rest_MediaController extends Zend_Rest_Controller
return; return;
} }
if (Application_Model_Systemstatus::isDiskOverQuota()) { try {
$sanitizedFile = CcFiles::createFromUpload($this->getRequest()->getPost());
$this->getResponse()
->setHttpResponseCode(201)
->appendBody(json_encode($sanitizedFile));
}
catch (InvalidMetadataException $e) {
$this->invalidDataResponse();
Logging::error($e->getMessage());
}
catch (OverDiskQuotaException $e) {
$this->getResponse() $this->getResponse()
->setHttpResponseCode(400) ->setHttpResponseCode(400)
->appendBody("ERROR: Disk Quota reached."); ->appendBody("ERROR: Disk Quota reached.");
return;
} }
catch (Exception $e) {
$file = new CcFiles(); $this->unknownErrorResponse();
$whiteList = $this->removeBlacklistedFieldsFromRequestData($this->getRequest()->getPost()); Logging::error($e->getMessage());
if (!$this->validateRequestData($file, $whiteList)) {
$file->setDbTrackTitle($_FILES["file"]["name"]);
$file->setDbUtime(new DateTime("now", new DateTimeZone("UTC")));
$file->save();
return;
} else {
/* If full_path is set, the post request came from ftp.
* Users are allowed to upload folders via ftp. If this is the case
* we need to include the folder name with the file name, otherwise
* files won't get removed from the organize folder.
*/
if (isset($whiteList["full_path"])) {
$fullPath = $whiteList["full_path"];
$basePath = isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."/srv/airtime/stor/organize/" : "/srv/airtime/stor/organize/";
//$relativePath is the folder name(if one) + track name, that was uploaded via ftp
$relativePath = substr($fullPath, strlen($basePath)-1);
} else {
$relativePath = $_FILES["file"]["name"];
}
$file->fromArray($whiteList);
$file->setDbOwnerId($this->getOwnerId());
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbTrackTitle($_FILES["file"]["name"]);
$file->setDbUtime($now);
$file->setDbHidden(true);
$file->save();
$callbackUrl = $this->getRequest()->getScheme() . '://' . $this->getRequest()->getHttpHost() . $this->getRequest()->getRequestUri() . "/" . $file->getPrimaryKey();
$this->processUploadedFile($callbackUrl, $relativePath, $this->getOwnerId());
$this->getResponse()
->setHttpResponseCode(201)
->appendBody(json_encode(CcFiles::sanitizeResponse($file)));
} }
} }
@ -158,89 +133,26 @@ class Rest_MediaController extends Zend_Rest_Controller
if (!$id) { if (!$id) {
return; return;
} }
$file = CcFilesQuery::create()->findPk($id);
// Since we check for this value when deleting files, set it first try {
//$file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); $requestData = json_decode($this->getRequest()->getRawBody(), true);
$sanitizedFile = CcFiles::updateFromArray($id, $requestData);
$requestData = json_decode($this->getRequest()->getRawBody(), true);
$whiteList = $this->removeBlacklistedFieldsFromRequestData($requestData);
$whiteList = $this->stripTimeStampFromYearTag($whiteList);
if (!$this->validateRequestData($file, $whiteList)) {
$file->save();
return;
} else if ($file && isset($requestData["resource_id"])) {
$file->fromArray($whiteList, BasePeer::TYPE_FIELDNAME);
//store the original filename
$file->setDbFilepath($requestData["filename"]);
$fileSizeBytes = $requestData["filesize"];
if (!isset($fileSizeBytes) || $fileSizeBytes === false)
{
$file->setDbImportStatus(2)->save();
$this->fileNotFoundResponse();
return;
}
$cloudFile = new CloudFile();
$cloudFile->setStorageBackend($requestData["storage_backend"]);
$cloudFile->setResourceId($requestData["resource_id"]);
$cloudFile->setCcFiles($file);
$cloudFile->save();
Application_Model_Preference::updateDiskUsage($fileSizeBytes);
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbMtime($now);
$file->save();
$this->getResponse() $this->getResponse()
->setHttpResponseCode(200) ->setHttpResponseCode(201)
->appendBody(json_encode(CcFiles::sanitizeResponse($file))); ->appendBody(json_encode($sanitizedFile));
} else if ($file) { }
catch (InvalidMetadataException $e) {
//local file storage $this->invalidDataResponse();
$file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); Logging::error($e->getMessage());
}
$file->fromArray($whiteList, BasePeer::TYPE_FIELDNAME); catch (FileNotFoundException $e) {
//Our RESTful API takes "full_path" as a field, which we then split and translate to match
//our internal schema. Internally, file path is stored relative to a directory, with the directory
//as a foreign key to cc_music_dirs.
if (isset($requestData["full_path"])) {
$fileSizeBytes = filesize($requestData["full_path"]);
if (!isset($fileSizeBytes) || $fileSizeBytes === false)
{
$file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save();
$this->fileNotFoundResponse();
return;
}
Application_Model_Preference::updateDiskUsage($fileSizeBytes);
$fullPath = $requestData["full_path"];
$storDir = Application_Model_MusicDir::getStorDir()->getDirectory();
$pos = strpos($fullPath, $storDir);
if ($pos !== FALSE)
{
assert($pos == 0); //Path must start with the stor directory path
$filePathRelativeToStor = substr($fullPath, strlen($storDir));
$file->setDbFilepath($filePathRelativeToStor);
}
}
$now = new DateTime("now", new DateTimeZone("UTC"));
$file->setDbMtime($now);
$file->save();
$this->getResponse()
->setHttpResponseCode(200)
->appendBody(json_encode(CcFiles::sanitizeResponse($file)));
} else {
$file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save();
$this->fileNotFoundResponse(); $this->fileNotFoundResponse();
Logging::error($e->getMessage());
}
catch (Exception $e) {
$this->unknownErrorResponse();
Logging::error($e->getMessage());
} }
} }
@ -250,16 +162,18 @@ class Rest_MediaController extends Zend_Rest_Controller
if (!$id) { if (!$id) {
return; return;
} }
$file = CcFilesQuery::create()->findPk($id); try {
if ($file) { CcFiles::deleteById($id);
$con = Propel::getConnection();
$storedFile = Application_Model_StoredFile::RecallById($id, $con);
$storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session?
$this->getResponse() $this->getResponse()
->setHttpResponseCode(204); ->setHttpResponseCode(204);
} else { }
catch (FileNotFoundException $e) {
$this->fileNotFoundResponse(); $this->fileNotFoundResponse();
Logging::error($e->getMessage());
}
catch (Exception $e) {
$this->unknownErrorResponse();
Logging::error($e->getMessage());
} }
} }
@ -288,177 +202,11 @@ class Rest_MediaController extends Zend_Rest_Controller
$resp->appendBody("ERROR: Import Failed."); $resp->appendBody("ERROR: Import Failed.");
} }
private function invalidDataResponse() private function unknownErrorResponse()
{ {
$resp = $this->getResponse(); $resp = $this->getResponse();
$resp->setHttpResponseCode(422); $resp->setHttpResponseCode(400);
$resp->appendBody("ERROR: Invalid data"); $resp->appendBody("An unknown error occurred.");
}
private function validateRequestData($file, &$whiteList)
{
// Sanitize any wildly incorrect metadata before it goes to be validated
FileDataHelper::sanitizeData($whiteList);
try {
// EditAudioMD form is used here for validation
$fileForm = new Application_Form_EditAudioMD();
$fileForm->startForm($file->getDbId());
$fileForm->populate($whiteList);
/*
* Here we are truncating metadata of any characters greater than the
* max string length set in the database. In the rare case a track's
* genre is more than 64 chars, for example, we don't want to reject
* tracks for that reason
*/
foreach($whiteList as $tag => &$value) {
if ($fileForm->getElement($tag)) {
$stringLengthValidator = $fileForm->getElement($tag)->getValidator('StringLength');
//$stringLengthValidator will be false if the StringLength validator doesn't exist on the current element
//in which case we don't have to truncate the extra characters
if ($stringLengthValidator) {
$value = substr($value, 0, $stringLengthValidator->getMax());
}
$value = $this->stripInvalidUtf8Characters($value);
}
}
if (!$fileForm->isValidPartial($whiteList)) {
throw new Exception("Data validation failed");
}
} catch (Exception $e) {
$errors = $fileForm->getErrors();
$messages = $fileForm->getMessages();
Logging::error($messages);
$file->setDbImportStatus(2);
$file->setDbHidden(true);
$this->invalidDataResponse();
return false;
}
return true;
}
private function processUploadedFile($callbackUrl, $originalFilename, $ownerId)
{
$CC_CONFIG = Config::getConfig();
$apiKey = $CC_CONFIG["apiKey"][0];
$tempFilePath = $_FILES['file']['tmp_name'];
$tempFileName = basename($tempFilePath);
//Only accept files with a file extension that we support.
$fileExtension = pathinfo($originalFilename, PATHINFO_EXTENSION);
if (!in_array(strtolower($fileExtension), explode(",", "ogg,mp3,oga,flac,wav,m4a,mp4,opus")))
{
@unlink($tempFilePath);
throw new Exception("Bad file extension.");
}
//TODO: Remove uploadFileAction from ApiController.php **IMPORTANT** - It's used by the recorder daemon...
$importedStorageDirectory = "";
if ($CC_CONFIG["current_backend"] == "file") {
$storDir = Application_Model_MusicDir::getStorDir();
$importedStorageDirectory = $storDir->getDirectory() . "/imported/" . $ownerId;
}
try {
//Copy the temporary file over to the "organize" folder so that it's off our webserver
//and accessible by airtime_analyzer which could be running on a different machine.
$newTempFilePath = Application_Model_StoredFile::copyFileToStor($tempFilePath, $originalFilename);
} catch (Exception $e) {
@unlink($tempFilePath);
Logging::error($e->getMessage());
return;
}
//Dispatch a message to airtime_analyzer through RabbitMQ,
//notifying it that there's a new upload to process!
$storageBackend = new ProxyStorageBackend($CC_CONFIG["current_backend"]);
Application_Model_RabbitMq::SendMessageToAnalyzer($newTempFilePath,
$importedStorageDirectory, basename($originalFilename),
$callbackUrl, $apiKey, $CC_CONFIG["current_backend"],
$storageBackend->getFilePrefix());
}
private function getOwnerId()
{
try {
if (Zend_Auth::getInstance()->hasIdentity()) {
$service_user = new Application_Service_UserService();
return $service_user->getCurrentUser()->getDbId();
} else {
$defaultOwner = CcSubjsQuery::create()
->filterByDbType(array('A', 'S'), Criteria::IN)
->orderByDbId()
->findOne();
if (!$defaultOwner) {
// what to do if there is no admin user?
// should we handle this case?
return null;
}
return $defaultOwner->getDbId();
}
} catch(Exception $e) {
Logging::info($e->getMessage());
}
}
/**
*
* Strips out fields from incoming request data that should never be modified
* from outside of Airtime
*
* @param array $data
*/
private static function removeBlacklistedFieldsFromRequestData($data) {
foreach (self::$blackList as $key) {
unset($data[$key]);
}
return $data;
}
private function removeEmptySubFolders($path) {
exec("find $path -empty -type d -delete");
}
/*
* It's possible that the year tag will be a timestamp but Airtime doesn't support this.
* The year field in cc_files can only be 16 chars max.
*
* This functions strips the year field of it's timestamp, if one, and leaves just the year
*/
private function stripTimeStampFromYearTag($metadata)
{
if (isset($metadata["year"])) {
if (preg_match("/^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/", $metadata["year"])) {
$metadata["year"] = substr($metadata["year"], 0, 4);
}
}
return $metadata;
}
private function stripInvalidUtf8Characters($string)
{
//Remove invalid UTF-8 characters
//reject overly long 2 byte sequences, as well as characters above U+10000 and replace with ?
$string = preg_replace('/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]'.
'|[\x00-\x7F][\x80-\xBF]+'.
'|([\xC0\xC1]|[\xF0-\xFF])[\x80-\xBF]*'.
'|[\xC2-\xDF]((?![\x80-\xBF])|[\x80-\xBF]{2,})'.
'|[\xE0-\xEF](([\x80-\xBF](?![\x80-\xBF]))|(?![\x80-\xBF]{2})|[\x80-\xBF]{3,})/S',
'?', $string );
//reject overly long 3 byte sequences and UTF-16 surrogates and replace with ?
$string = preg_replace('/\xE0[\x80-\x9F][\x80-\xBF]'.
'|\xED[\xA0-\xBF][\x80-\xBF]/S','?', $string );
//Do a final encoding conversion to
$string = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
return $string;
} }
} }

View File

@ -0,0 +1,89 @@
<?php
require_once('ProxyStorageBackend.php');
require_once("FileIO.php");
class Application_Service_MediaService
{
/** Move (or copy) a file to the stor/organize directory and send it off to the
analyzer to be processed.
* @param $callbackUrl
* @param $filePath string Path to the local file to import to the library
* @param $originalFilename string The original filename, if you want it to be preserved after import.
* @param $ownerId string The ID of the user that will own the file inside Airtime.
* @param $copyFile bool True if you want to copy the file to the "organize" directory, false if you want to move it (default)
* @return Ambigous
* @throws Exception
*/
public static function importFileToLibrary($callbackUrl, $filePath, $originalFilename, $ownerId, $copyFile)
{
$CC_CONFIG = Config::getConfig();
$apiKey = $CC_CONFIG["apiKey"][0];
$importedStorageDirectory = "";
if ($CC_CONFIG["current_backend"] == "file") {
$storDir = Application_Model_MusicDir::getStorDir();
$importedStorageDirectory = $storDir->getDirectory() . "/imported/" . $ownerId;
}
//Copy the temporary file over to the "organize" folder so that it's off our webserver
//and accessible by airtime_analyzer which could be running on a different machine.
$newTempFilePath = Application_Model_StoredFile::moveFileToStor($filePath, $originalFilename, $copyFile);
//Dispatch a message to airtime_analyzer through RabbitMQ,
//notifying it that there's a new upload to process!
$storageBackend = new ProxyStorageBackend($CC_CONFIG["current_backend"]);
Application_Model_RabbitMq::SendMessageToAnalyzer($newTempFilePath,
$importedStorageDirectory, basename($originalFilename),
$callbackUrl, $apiKey,
$CC_CONFIG["current_backend"],
$storageBackend->getFilePrefix());
return $newTempFilePath;
}
/**
* @param $fileId
* @param bool $inline Set the Content-Disposition header to inline to prevent a download dialog from popping up (or attachment if false)
* @throws Exception
* @throws FileNotFoundException
*/
public static function streamFileDownload($fileId, $inline=false)
{
$media = Application_Model_StoredFile::RecallById($fileId);
if ($media == null) {
throw new FileNotFoundException();
}
$filepath = $media->getFilePath();
// Make sure we don't have some wrong result beecause of caching
clearstatcache();
if ($media->getPropelOrm()->isValidPhysicalFile()) {
$filename = $media->getPropelOrm()->getFilename();
//Download user left clicks a track and selects Download.
if (!$inline) {
//We are using Content-Disposition to specify
//to the browser what name the file should be saved as.
header('Content-Disposition: attachment; filename="' . $filename . '"');
} else {
//user clicks play button for track and downloads it.
header('Content-Disposition: inline; filename="' . $filename . '"');
}
$filepath = $media->getFilePath();
$size= $media->getFileSize();
$mimeType = $media->getPropelOrm()->getDbMime();
Application_Common_FileIO::smartReadFile($filepath, $size, $mimeType);
exit;
} else {
throw new FileNotFoundException();
}
}
}

View File

@ -341,3 +341,44 @@ class AirtimeUpgrader2510 extends AirtimeUpgrader
} }
} }
} }
class AirtimeUpgrader2511 extends AirtimeUpgrader
{
protected function getSupportedVersions() {
return array (
'2.5.10'
);
}
public function getNewVersion() {
return '2.5.11';
}
public function upgrade($dir = __DIR__) {
Cache::clear();
assert($this->checkIfUpgradeSupported());
$newVersion = $this->getNewVersion();
try {
$this->toggleMaintenanceScreen(true);
Cache::clear();
// Begin upgrade
$queryResult = CcFilesQuery::create()
->select(array('disk_usage'))
->withColumn('SUM(CcFiles.filesize)', 'disk_usage')
->find();
$disk_usage = $queryResult[0];
Application_Model_Preference::setDiskUsage($disk_usage);
Application_Model_Preference::SetAirtimeVersion($newVersion);
Cache::clear();
$this->toggleMaintenanceScreen(false);
} catch(Exception $e) {
$this->toggleMaintenanceScreen(false);
throw $e;
}
}
}

View File

@ -7,10 +7,12 @@
<?php echo $this->element->getElement('stationLogo')->render() ?> <?php echo $this->element->getElement('stationLogo')->render() ?>
<button id="logo-remove-btn" value="Remove" class="btn">Remove</button> <?php echo $this->element->getElement('stationLogoRemove')->render() ?>
<!-- <button id="logo-remove-btn" value="Remove" class="btn">Remove</button>-->
<div id="Logo-img-container"> <div id="Logo-img-container">
<img onError="this.onerror = '';this.style.visibility='hidden';$('#logo-remove-btn').hide();" id="logo-img" onload='resizeImg(this, 450, 450);' src="data:image/png;base64,<?php echo $this->element->getView()->logoImg ?>" /> <img onError="this.onerror = '';this.style.visibility='hidden';$('#logo-remove-btn').hide();$('[id^=stationLogoRemove]').each(function(i,v){v.style.width=v.style.height=v.style.margin=v.style.padding='0px';});" id="logo-img" onload='resizeImg(this, 450, 450);' src="data:image/png;base64,<?php echo $this->element->getView()->logoImg ?>" />
</div> </div>
<?php echo $this->element->getElement('locale')->render() ?> <?php echo $this->element->getElement('locale')->render() ?>

View File

@ -1,4 +1,4 @@
<?php if($this->trial_remain != '' && $this->trial_remain != "Trial expired."){?> <?php if($this->is_trial && $this->trial_remain != '' && $this->trial_remain != "Trial expired."){?>
<div class="trial-box"> <div class="trial-box">
<p><?php echo _("Your trial expires in") ?></p> <p><?php echo _("Your trial expires in") ?></p>
<div class="trial-box-calendar"> <div class="trial-box-calendar">

View File

@ -2197,12 +2197,17 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio
height: 120px; height: 120px;
} }
.preferences #stationLogoRemove-label {
display: none;
}
.preferences #logo-remove-btn { .preferences #logo-remove-btn {
float: right; float: right;
margin-bottom: 4px;
} }
.preferences #Logo-img-container { .preferences #Logo-img-container {
float: left; margin-top: 30px;
} }
#show_time_info { #show_time_info {

View File

@ -54,6 +54,9 @@ set_include_path(APPLICATION_PATH . '/controllers' . PATH_SEPARATOR . get_includ
//Controller plugins. //Controller plugins.
set_include_path(APPLICATION_PATH . '/controllers/plugins' . PATH_SEPARATOR . get_include_path()); set_include_path(APPLICATION_PATH . '/controllers/plugins' . PATH_SEPARATOR . get_include_path());
//Services.
set_include_path(APPLICATION_PATH . '/services/' . PATH_SEPARATOR . get_include_path());
//Zend framework //Zend framework
if (file_exists('/usr/share/php/libzend-framework-php')) { if (file_exists('/usr/share/php/libzend-framework-php')) {
set_include_path('/usr/share/php/libzend-framework-php' . PATH_SEPARATOR . get_include_path()); set_include_path('/usr/share/php/libzend-framework-php' . PATH_SEPARATOR . get_include_path());

View File

@ -91,16 +91,11 @@ function openAudioPreview(p_event) {
} }
} }
function open_audio_preview(type, id, audioFileTitle, audioFileArtist) { function open_audio_preview(type, id) {
// we need to remove soundcloud icon from audioFileTitle
var index = audioFileTitle.indexOf("<span class=");
if(index != -1){
audioFileTitle = audioFileTitle.substring(0,index);
}
// The reason that we need to encode artist and title string is that // The reason that we need to encode artist and title string is that
// sometime they contain '/' or '\' and apache reject %2f or %5f // sometime they contain '/' or '\' and apache reject %2f or %5f
// so the work around is to encode it twice. // so the work around is to encode it twice.
openPreviewWindow(baseUrl+'audiopreview/audio-preview/audioFileID/'+id+'/audioFileArtist/'+encodeURIComponent(encodeURIComponent(audioFileArtist))+'/audioFileTitle/'+encodeURIComponent(encodeURIComponent(audioFileTitle))+'/type/'+type); openPreviewWindow(baseUrl+'audiopreview/audio-preview/audioFileID/'+id+'/type/'+type);
_preview_window.focus(); _preview_window.focus();
} }

View File

@ -652,11 +652,11 @@ var AIRTIME = (function(AIRTIME) {
open_playlist_preview(aData.audioFile, 0); open_playlist_preview(aData.audioFile, 0);
} else if (aData.ftype === 'audioclip') { } else if (aData.ftype === 'audioclip') {
if (isAudioSupported(aData.mime)) { if (isAudioSupported(aData.mime)) {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); open_audio_preview(aData.ftype, aData.id);
} }
} else if (aData.ftype == 'stream') { } else if (aData.ftype == 'stream') {
if (isAudioSupported(aData.mime)) { if (isAudioSupported(aData.mime)) {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); open_audio_preview(aData.ftype, aData.id);
} }
} else if (aData.ftype == 'block' && aData.bl_type == 'static') { } else if (aData.ftype == 'block' && aData.bl_type == 'static') {
open_block_preview(aData.audioFile, 0); open_block_preview(aData.audioFile, 0);
@ -964,7 +964,7 @@ var AIRTIME = (function(AIRTIME) {
// pl_ // pl_
open_playlist_preview(playlistIndex, 0); open_playlist_preview(playlistIndex, 0);
} else if (data.ftype === 'audioclip' || data.ftype === 'stream') { } else if (data.ftype === 'audioclip' || data.ftype === 'stream') {
open_audio_preview(data.ftype, data.audioFile, data.track_title, data.artist_name); open_audio_preview(data.ftype, data.id);
} else if (data.ftype === 'block') { } else if (data.ftype === 'block') {
blockIndex = $(this).parent().attr('id').substring(3); // remove blockIndex = $(this).parent().attr('id').substring(3); // remove
// the // the

View File

@ -115,6 +115,11 @@ function setSoundCloudCheckBoxListener() {
}); });
} }
function removeLogo() {
$.post(baseUrl+'Preference/remove-logo', function(json){});
location.reload();
}
$(document).ready(function() { $(document).ready(function() {
$('.collapsible-header').live('click',function() { $('.collapsible-header').live('click',function() {
@ -123,10 +128,6 @@ $(document).ready(function() {
return false; return false;
}).next().hide(); }).next().hide();
$('#logo-remove-btn').click(function() {
$.post(baseUrl+'Preference/remove-logo', function(json){});
});
/* No longer using AJAX for this form. Zend + our code makes it needlessly hard to deal with. -- Albert /* No longer using AJAX for this form. Zend + our code makes it needlessly hard to deal with. -- Albert
$('#pref_save').live('click', function() { $('#pref_save').live('click', function() {
var data = $('#pref_form').serialize(); var data = $('#pref_form').serialize();

View File

@ -668,7 +668,7 @@ function setAddShowEvents(form) {
var showId = $("#add_show_id").attr("value"); var showId = $("#add_show_id").attr("value");
if (showId && $("#add_show_logo_current").attr("src") !== "") { if (showId && $("#add_show_logo_current").attr("src") !== "") {
var action = '/rest/show-image?id=' + showId; var action = '/rest/show-image?csrf_token=' + $('#csrf').val() + '&id=' + showId;
$.ajax({ $.ajax({
url: action, url: action,
@ -748,7 +748,7 @@ function setAddShowEvents(form) {
data: {format: "json", data: data, hosts: hosts, days: days}, data: {format: "json", data: data, hosts: hosts, days: days},
success: function(json) { success: function(json) {
if (json.showId && image) { // Successfully added the show, and it contains an image to upload if (json.showId && image) { // Successfully added the show, and it contains an image to upload
var imageAction = '/rest/show-image?id=' + json.showId; var imageAction = '/rest/show-image?csrf_token=' + $('#csrf').val() + '&id=' + json.showId;
// perform a second xhttprequest in order to send the show image // perform a second xhttprequest in order to send the show image
$.ajax({ $.ajax({

View File

@ -22,7 +22,7 @@ class AnalyzerPipeline:
""" """
@staticmethod @staticmethod
def run_analysis(queue, audio_file_path, import_directory, original_filename, file_prefix, cloud_storage_config): 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. """Analyze and import an audio file, and put all extracted metadata into queue.
Keyword arguments: Keyword arguments:
@ -35,6 +35,7 @@ class AnalyzerPipeline:
preserve. The file at audio_file_path typically has a preserve. The file at audio_file_path typically has a
temporary randomly generated name, which is why we want temporary randomly generated name, which is why we want
to know what the original name was. to know what the original name was.
storage_backend: String indicating the storage backend (amazon_s3 or file)
file_prefix: file_prefix:
cloud_storage_config: ConfigParser object containing the cloud storage configuration settings cloud_storage_config: ConfigParser object containing the cloud storage configuration settings
""" """
@ -68,8 +69,8 @@ class AnalyzerPipeline:
metadata = ReplayGainAnalyzer.analyze(audio_file_path, metadata) metadata = ReplayGainAnalyzer.analyze(audio_file_path, metadata)
metadata = PlayabilityAnalyzer.analyze(audio_file_path, metadata) metadata = PlayabilityAnalyzer.analyze(audio_file_path, metadata)
csu = CloudStorageUploader(cloud_storage_config) if storage_backend.lower() == u"amazon_s3":
if csu.enabled(): csu = CloudStorageUploader(cloud_storage_config)
metadata = csu.upload_obj(audio_file_path, metadata) metadata = csu.upload_obj(audio_file_path, metadata)
else: else:
metadata = FileMoverAnalyzer.move(audio_file_path, import_directory, original_filename, metadata) metadata = FileMoverAnalyzer.move(audio_file_path, import_directory, original_filename, metadata)

View File

@ -169,9 +169,9 @@ class MessageListener:
import_directory = msg_dict["import_directory"] import_directory = msg_dict["import_directory"]
original_filename = msg_dict["original_filename"] original_filename = msg_dict["original_filename"]
file_prefix = msg_dict["file_prefix"] file_prefix = msg_dict["file_prefix"]
storage_backend = msg_dict["storage_backend"]
audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename, file_prefix, self.cloud_storage_config) audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename, storage_backend, file_prefix, self.cloud_storage_config)
StatusReporter.report_success_to_callback_url(callback_url, api_key, audio_metadata) StatusReporter.report_success_to_callback_url(callback_url, api_key, audio_metadata)
except KeyError as e: except KeyError as e:
@ -210,11 +210,11 @@ class MessageListener:
channel.basic_ack(delivery_tag=method_frame.delivery_tag) channel.basic_ack(delivery_tag=method_frame.delivery_tag)
@staticmethod @staticmethod
def spawn_analyzer_process(audio_file_path, import_directory, original_filename, file_prefix, cloud_storage_config): def spawn_analyzer_process(audio_file_path, import_directory, original_filename, storage_backend, file_prefix, cloud_storage_config):
''' Spawn a child process to analyze and import a new audio file. ''' ''' Spawn a child process to analyze and import a new audio file. '''
q = multiprocessing.Queue() q = multiprocessing.Queue()
p = multiprocessing.Process(target=AnalyzerPipeline.run_analysis, p = multiprocessing.Process(target=AnalyzerPipeline.run_analysis,
args=(q, audio_file_path, import_directory, original_filename, file_prefix, cloud_storage_config)) args=(q, audio_file_path, import_directory, original_filename, storage_backend, file_prefix, cloud_storage_config))
p.start() p.start()
p.join() p.join()
if p.exitcode == 0: if p.exitcode == 0:

View File

@ -28,6 +28,18 @@ class MetadataAnalyzer(Analyzer):
#Other fields we'll want to set for Airtime: #Other fields we'll want to set for Airtime:
metadata["hidden"] = False metadata["hidden"] = False
# Get file size and md5 hash of the file
metadata["filesize"] = os.path.getsize(filename)
with open(filename, 'rb') as fh:
m = hashlib.md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
metadata["md5"] = m.hexdigest()
# Mutagen doesn't handle WAVE files so we use a different package # Mutagen doesn't handle WAVE files so we use a different package
mime_check = magic.from_file(filename, mime=True) mime_check = magic.from_file(filename, mime=True)
metadata["mime"] = mime_check metadata["mime"] = mime_check
@ -98,20 +110,6 @@ class MetadataAnalyzer(Analyzer):
#If we couldn't figure out the track_number or track_total, just ignore it... #If we couldn't figure out the track_number or track_total, just ignore it...
pass pass
# Get file size and md5 hash of the file
metadata["filesize"] = os.path.getsize(filename)
with open(filename, 'rb') as fh:
m = hashlib.md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
metadata["md5"] = m.hexdigest()
#We normalize the mutagen tags slightly here, so in case mutagen changes, #We normalize the mutagen tags slightly here, so in case mutagen changes,
#we find the #we find the
mutagen_to_airtime_mapping = { mutagen_to_airtime_mapping = {

View File

@ -35,7 +35,7 @@ post_file() {
# -f is needed to make curl fail if there's an HTTP error code # -f is needed to make curl fail if there's an HTTP error code
# -L is needed to follow redirects! (just in case) # -L is needed to follow redirects! (just in case)
until curl -fL --max-time 30 $url -u $api_key":" -X POST -F "file=@${file_path}" -F "full_path=${file_path}" until curl -fL --max-time 30 $url -u $api_key":" -X POST -F "file=@${file_path}"
do do
retry_count=$[$retry_count+1] retry_count=$[$retry_count+1]
if [ $retry_count -ge $max_retry ]; then if [ $retry_count -ge $max_retry ]; then

View File

@ -167,7 +167,7 @@ class PypoFile(Thread):
except IOError as e: except IOError as e:
logging.debug("Failed to open config file at %s: %s" % (config_path, e.strerror)) logging.debug("Failed to open config file at %s: %s" % (config_path, e.strerror))
sys.exit() sys.exit()
except Exception: except Exception as e:
logging.debug(e.strerror) logging.debug(e.strerror)
sys.exit() sys.exit()