From 0177e400836a235e6ce525a99efe56b8e9fc70fd Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 19 Feb 2015 15:10:01 -0500 Subject: [PATCH 01/46] Pull the logic for Media REST API out of the controller --- airtime_mvc/application/common/HTTPHelper.php | 18 + .../application/models/airtime/CcFiles.php | 366 +++++++++++++++- .../rest/controllers/MediaController.php | 389 +++--------------- .../application/services/MediaService.php | 41 ++ airtime_mvc/public/index.php | 3 + 5 files changed, 482 insertions(+), 335 deletions(-) create mode 100644 airtime_mvc/application/services/MediaService.php diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index db314bb0b..505befd7e 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -17,4 +17,22 @@ class Application_Common_HTTPHelper $request->getParam("timezone", null) ); } + + public static function getStationUrl() + { + $scheme = "http"; + if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $scheme = "https"; + } + $CC_CONFIG = Config::getConfig(); + $baseUrl = $CC_CONFIG['baseUrl']; + $baseDir = $CC_CONFIG['baseDir']; + $basePort = $CC_CONFIG['basePort']; + if (empty($baseDir)) { + $baseDir = "/"; + } + $stationUrl = "$scheme://${baseUrl}:${basePort}${baseDir}"; + + return $stationUrl; + } } diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 93ef491a8..554139e40 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -11,17 +11,288 @@ * * @package propel.generator.campcaster */ + +class InvalidMetadataException extends Exception +{ +} + +class FileNotFoundException extends Exception +{ +} + +class OverDiskQuotaException extends Exception +{ + +} + class CcFiles extends BaseCcFiles { - + + const MUSIC_DIRS_STOR_PK = 1; + + + //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 private static $privateFields = array( - 'file_exists', - 'silan_check', - 'is_scheduled', - 'is_playlist' + 'file_exists', + 'silan_check', + 'is_scheduled', + 'is_playlist' ); - - public function getCueLength() + + /** + * Retrieve a sanitized version of the file metadata, suitable for public access. + * @param $fileId + */ + public static function getSantiziedFileById($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(); + } + + $file = new CcFiles(); + + try{ + $fileArray = self::removeBlacklistedFields($fileArray); + + /*if (!self::validateFileArray($fileArray)) + { + $file->setDbTrackTitle($_FILES["file"]["name"]); + $file->setDbUtime(new DateTime("now", new DateTimeZone("UTC"))); + $file->save(); + return CcFiles::sanitizeResponse($file);*/ + self::validateFileArray($fileArray); + + /* 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($fileArray["full_path"])) { + $fullPath = $fileArray["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($fileArray); + $file->setDbOwnerId(self::getOwnerId()); + $now = new DateTime("now", new DateTimeZone("UTC")); + $file->setDbTrackTitle($_FILES["file"]["name"]); + $file->setDbUtime($now); + $file->setDbHidden(true); + $file->save(); + + $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "/rest/media/" . $file->getPrimaryKey(); + + Application_Service_MediaService::processUploadedFile($callbackUrl, $relativePath, self::getOwnerId()); + return CcFiles::sanitizeResponse($file); + + } catch (Exception $e) { + $file->setDbImportStatus(2); + $file->setDbHidden(true); + throw $e; + } + } + + /** Update a file with metadata specified in an array. + * @param $fileId The ID of the file to update in the DB. + * @param $fileArray 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); + + // Since we check for this value when deleting files, set it first + $file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); + + $fileArray = self::removeBlacklistedFields($fileArray); + $fileArray = self::stripTimeStampFromYearTag($fileArray); + + self::validateFileArray($fileArray); + if ($file && isset($requestData["resource_id"])) { + + $file->fromArray($fileArray, BasePeer::TYPE_FIELDNAME); + + //store the original filename + $file->setDbFilepath($fileArray["filename"]); + + $fileSizeBytes = $fileArray["filesize"]; + if (!isset($fileSizeBytes) || $fileSizeBytes === false) + { + $file->setDbImportStatus(2)->save(); + throw new FileNotFoundException(); + } + $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) { + + $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) + { + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + throw new FileNotFoundException(); + } + 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(); + + /* $this->removeEmptySubFolders( + isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."/srv/airtime/stor/organize/" : "/srv/airtime/stor/organize/"); */ + } else { + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + throw new FileNotFoundException(); + } + + 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 = new Application_Model_StoredFile($file, $con); + if ($storedFile->existsOnDisk()) { + $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? + } + $file->delete(); + } else { + throw new FileNotFoundException(); + } + + } + + public static function getDownloadUrl($id) + { + $file = CcFilesQuery::create()->findPk($id); + if ($file) { + $con = Propel::getConnection(); + $storedFile = new Application_Model_StoredFile($file, $con); + $baseDir = Application_Common_OsPath::getBaseDir(); + + return $storedFile->getRelativeFileUrl($baseDir) . '/download/true'; + } + 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(); $cueout = $this->getDbCueout(); @@ -70,4 +341,83 @@ class CcFiles extends BaseCcFiles { return $response; } -} // CcFiles + + /** + * + * 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"); + } +} diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 90587d480..08240dc80 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -2,26 +2,10 @@ 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() { @@ -54,17 +38,19 @@ class Rest_MediaController extends Zend_Rest_Controller return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - $con = Propel::getConnection(); - $storedFile = new Application_Model_StoredFile($file, $con); - $baseUrl = Application_Common_OsPath::getBaseDir(); - + try + { $this->getResponse() ->setHttpResponseCode(200) - ->appendBody($this->_redirect($storedFile->getRelativeFileUrl($baseUrl).'/download/true')); - } else { + ->appendBody($this->_redirect(CcFiles::getDownloadUrl($id))); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -75,14 +61,18 @@ class Rest_MediaController extends Zend_Rest_Controller return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - + try { $this->getResponse() ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::sanitizeResponse($file))); - } else { + ->appendBody(json_encode(CcFiles::getSantiziedFileById($id))); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -97,52 +87,24 @@ class Rest_MediaController extends Zend_Rest_Controller 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() ->setHttpResponseCode(400) ->appendBody("ERROR: Disk Quota reached."); - return; } - - $file = new CcFiles(); - $whiteList = $this->removeBlacklistedFieldsFromRequestData($this->getRequest()->getPost()); - - 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))); + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -152,90 +114,25 @@ class Rest_MediaController extends Zend_Rest_Controller if (!$id) { return; } - - $file = CcFilesQuery::create()->findPk($id); - // Since we check for this value when deleting files, set it first - $file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); - $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(); - + try { + $requestData = json_decode($this->getRequest()->getRawBody(), true); + $sanitizedFile = CcFiles::updateFromArray($id, $requestData); $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::sanitizeResponse($file))); - } else if ($file) { - - $file->fromArray($whiteList, 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($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->removeEmptySubFolders( - isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."/srv/airtime/stor/organize/" : "/srv/airtime/stor/organize/"); */ - - $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::sanitizeResponse($file))); - } else { - $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + ->setHttpResponseCode(201) + ->appendBody(json_encode($sanitizedFile)); + } + catch (InvalidMetadataException $e) { + $this->invalidDataResponse(); + Logging::error($e->getMessage()); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -245,18 +142,19 @@ class Rest_MediaController extends Zend_Rest_Controller if (!$id) { return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - $con = Propel::getConnection(); - $storedFile = new Application_Model_StoredFile($file, $con); - if ($storedFile->existsOnDisk()) { - $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? - } - $file->delete(); + + try { + CcFiles::deleteById($id); $this->getResponse() ->setHttpResponseCode(204); - } else { + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -278,174 +176,11 @@ class Rest_MediaController extends Zend_Rest_Controller $resp->appendBody("ERROR: Media not found."); } - private function invalidDataResponse() + private function unknownErrorResponse() { $resp = $this->getResponse(); - $resp->setHttpResponseCode(422); - $resp->appendBody("ERROR: Invalid data"); - } - - 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... - - $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! - Application_Model_RabbitMq::SendMessageToAnalyzer($newTempFilePath, - $importedStorageDirectory, basename($originalFilename), - $callbackUrl, $apiKey); - } - - 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('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()); - } - } - - /** - * - * 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; + $resp->setHttpResponseCode(400); + $resp->appendBody("An unknown error occurred."); } } diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php new file mode 100644 index 000000000..74b2609d0 --- /dev/null +++ b/airtime_mvc/application/services/MediaService.php @@ -0,0 +1,41 @@ +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! + Application_Model_RabbitMq::SendMessageToAnalyzer($newTempFilePath, + $importedStorageDirectory, basename($originalFilename), + $callbackUrl, $apiKey); + } +} \ No newline at end of file diff --git a/airtime_mvc/public/index.php b/airtime_mvc/public/index.php index f9c691f51..c45ac3b1d 100644 --- a/airtime_mvc/public/index.php +++ b/airtime_mvc/public/index.php @@ -49,6 +49,9 @@ set_include_path(APPLICATION_PATH . '/models' . PATH_SEPARATOR . get_include_pat //Controller plugins. 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 if (file_exists('/usr/share/php/libzend-framework-php')) { set_include_path('/usr/share/php/libzend-framework-php' . PATH_SEPARATOR . get_include_path()); From be7cae44087188edb3c9b77dfb2a1d8f7dbe8ed2 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Fri, 20 Feb 2015 13:12:49 -0500 Subject: [PATCH 02/46] Fixed CSRF prevention checks for REST calls, moved CSRF initialization to Bootstrap --- airtime_mvc/application/Bootstrap.php | 17 ++++++++++++++++- .../controllers/LoginController.php | 3 +++ .../controllers/PluploadController.php | 5 +++-- .../controllers/plugins/Acl_plugin.php | 19 +++++++++++-------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index b4a4fb43a..787bc5d03 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -65,13 +65,28 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap } $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 * from a php init function. This will save us from having to * reinitialize them every request */ - private function _initTranslationGlobals($view) { + protected function _initTranslationGlobals() { + $view = $this->getResource('view'); $view->headScript()->appendScript("var PRODUCT_NAME = '" . PRODUCT_NAME . "';"); $view->headScript()->appendScript("var USER_MANUAL_URL = '" . USER_MANUAL_URL . "';"); $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';"); diff --git a/airtime_mvc/application/controllers/LoginController.php b/airtime_mvc/application/controllers/LoginController.php index c808c2aee..7566adb6f 100644 --- a/airtime_mvc/application/controllers/LoginController.php +++ b/airtime_mvc/application/controllers/LoginController.php @@ -98,6 +98,9 @@ class LoginController extends Zend_Controller_Action { $auth = Zend_Auth::getInstance(); $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'); } diff --git a/airtime_mvc/application/controllers/PluploadController.php b/airtime_mvc/application/controllers/PluploadController.php index f1197ec2b..6aae736ae 100644 --- a/airtime_mvc/application/controllers/PluploadController.php +++ b/airtime_mvc/application/controllers/PluploadController.php @@ -32,8 +32,9 @@ class PluploadController extends Zend_Controller_Action } $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); - $csrf_namespace->setExpirationSeconds(5*60*60); - $csrf_namespace->authtoken = sha1(uniqid(rand(),1)); + /* Moved to be globally set in Bootstrap */ + // $csrf_namespace->setExpirationSeconds(5*60*60); + // $csrf_namespace->authtoken = sha1(uniqid(rand(),1)); $csrf_element = new Zend_Form_Element_Hidden('csrf'); $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); diff --git a/airtime_mvc/application/controllers/plugins/Acl_plugin.php b/airtime_mvc/application/controllers/plugins/Acl_plugin.php index bd803ec89..fbf783131 100644 --- a/airtime_mvc/application/controllers/plugins/Acl_plugin.php +++ b/airtime_mvc/application/controllers/plugins/Acl_plugin.php @@ -148,17 +148,22 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract } } } else { //We have a session/identity. - // If we have an identity and we're making a RESTful request, // we need to check the CSRF token - if ($request->_action != "get" && $request->getModuleName() == "rest") { - $tokenValid = $this->verifyCSRFToken($request->getParam("csrf_token")); + if ($_SERVER['REQUEST_METHOD'] != "GET" && $request->getModuleName() == "rest") { + $token = $request->getParam("csrf_token"); + $tokenValid = $this->verifyCSRFToken($token); if (!$tokenValid) { + $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); + $csrf_namespace->authtoken = sha1(uniqid(rand(),1)); + + Logging::warn("Invalid CSRF token: $token"); $this->getResponse() ->setHttpResponseCode(401) - ->appendBody("ERROR: CSRF token mismatch."); - return; + ->appendBody("ERROR: CSRF token mismatch.") + ->sendResponse(); + die(); } } @@ -202,9 +207,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract $current_namespace = new Zend_Session_Namespace('csrf_namespace'); $observed_csrf_token = $token; $expected_csrf_token = $current_namespace->authtoken; - Logging::error("Observed: " . $observed_csrf_token); - Logging::error("Expected: " . $expected_csrf_token); - + return ($observed_csrf_token == $expected_csrf_token); } From 2a89e4d5a098b7cb86de3c416d7017622499b433 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Fri, 20 Feb 2015 14:01:06 -0500 Subject: [PATCH 03/46] Massive refactor of the analyzer branch and sync it back up with the cloud storage branch (for the last time) * Backported all the bugfixes from cc-5709-airtime-analyzer-cloud-storage * Backported missing FileStorageBackend.php * Fixed CC-6001: Track titles and artist names with slashes break audio preview * Refactored all the MediaController code, pulling out the logic into MediaService * Fixed an API key leak to guests in the Media API * Made this branch work without cloud_storage.conf (defaults to file storage) * Made ApiController's getMediaAction use the MediaService code --- ...zon_S3.php => Amazon_S3StorageBackend.php} | 4 +- .../cloud_storage/FileStorageBackend.php | 41 ++++++ .../cloud_storage/ProxyStorageBackend.php | 36 +++-- .../cloud_storage/StorageBackend.php | 10 ++ airtime_mvc/application/configs/conf.php | 25 ++-- .../application/controllers/ApiController.php | 92 +------------ .../controllers/AudiopreviewController.php | 9 +- airtime_mvc/application/models/Schedule.php | 1 - .../application/models/airtime/CcFiles.php | 88 ++++++------ .../rest/controllers/MediaController.php | 7 +- .../application/services/MediaService.php | 126 +++++++++++++++++- .../public/js/airtime/common/common.js | 9 +- .../public/js/airtime/library/library.js | 6 +- 13 files changed, 275 insertions(+), 179 deletions(-) rename airtime_mvc/application/cloud_storage/{Amazon_S3.php => Amazon_S3StorageBackend.php} (93%) create mode 100644 airtime_mvc/application/cloud_storage/FileStorageBackend.php diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php similarity index 93% rename from airtime_mvc/application/cloud_storage/Amazon_S3.php rename to airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php index 978e1ba07..24e398af0 100644 --- a/airtime_mvc/application/cloud_storage/Amazon_S3.php +++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php @@ -4,12 +4,12 @@ require_once 'StorageBackend.php'; use Aws\S3\S3Client; -class Amazon_S3 extends StorageBackend +class Amazon_S3StorageBackend extends StorageBackend { private $s3Client; - public function Amazon_S3($securityCredentials) + public function Amazon_S3StorageBackend($securityCredentials) { $this->setBucket($securityCredentials['bucket']); $this->setAccessKey($securityCredentials['api_key']); diff --git a/airtime_mvc/application/cloud_storage/FileStorageBackend.php b/airtime_mvc/application/cloud_storage/FileStorageBackend.php new file mode 100644 index 000000000..65df4d55b --- /dev/null +++ b/airtime_mvc/application/cloud_storage/FileStorageBackend.php @@ -0,0 +1,41 @@ +storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]); + if ($storageBackend == "amazon_S3") { + $this->storageBackend = new Amazon_S3StorageBackend($CC_CONFIG["amazon_S3"]); + } else if ($storageBackend == "file") { + $this->storageBackend = new FileStorageBackend(); + } else { + $this->storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]); + } } - + public function getAbsoluteFilePath($resourceId) { return $this->storageBackend->getAbsoluteFilePath($resourceId); } - + public function getSignedURL($resourceId) { return $this->storageBackend->getSignedURL($resourceId); } - + public function getFileSize($resourceId) { return $this->storageBackend->getFileSize($resourceId); } - + public function deletePhysicalFile($resourceId) { $this->storageBackend->deletePhysicalFile($resourceId); } -} + public function deleteAllCloudFileObjects() + { + $this->storageBackend->deleteAllCloudFileObjects(); + } + + public function getFilePrefix() + { + return $this->storageBackend->getFilePrefix(); + } +} \ No newline at end of file diff --git a/airtime_mvc/application/cloud_storage/StorageBackend.php b/airtime_mvc/application/cloud_storage/StorageBackend.php index 84a9a8d72..a9547d3ee 100644 --- a/airtime_mvc/application/cloud_storage/StorageBackend.php +++ b/airtime_mvc/application/cloud_storage/StorageBackend.php @@ -52,4 +52,14 @@ abstract class StorageBackend { $this->secretKey = $secretKey; } + + public function deleteAllCloudFileObjects() + { + return false; + } + + public function getFilePrefix() + { + return ""; + } } diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index 6b6273a22..3bd092a1e 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -27,17 +27,22 @@ class Config { // Parse separate conf file for cloud storage values $cloudStorageConfig = isset($_SERVER['CLOUD_STORAGE_CONF']) ? $_SERVER['CLOUD_STORAGE_CONF'] : "/etc/airtime-saas/cloud_storage.conf"; - $cloudStorageValues = parse_ini_file($cloudStorageConfig, true); - - $supportedStorageBackends = array('amazon_S3'); - foreach ($supportedStorageBackends as $backend) { - $CC_CONFIG[$backend] = $cloudStorageValues[$backend]; + $cloudStorageValues = @parse_ini_file($cloudStorageConfig, true); + if ($cloudStorageValues !== false) { + $supportedStorageBackends = array('amazon_S3'); + foreach ($supportedStorageBackends as $backend) { + $CC_CONFIG[$backend] = $cloudStorageValues[$backend]; + } + // Tells us where file uploads will be uploaded to. + // It will either be set to a cloud storage backend or local file storage. + $CC_CONFIG["current_backend"] = $cloudStorageValues["current_backend"]["storage_backend"]; + + } else { + //Default to file storage if we didn't find a cloud_storage.conf + $CC_CONFIG["current_backend"] = "file"; } - - // Tells us where file uploads will be uploaded to. - // It will either be set to a cloud storage backend or local file storage. - $CC_CONFIG["current_backend"] = $cloudStorageValues["current_backend"]["storage_backend"]; - + + $values = parse_ini_file($filename, true); // Name of the web server user diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index c5d76cf06..9da4b4add 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -93,100 +93,12 @@ class ApiController extends Zend_Controller_Action $fileId = $this->_getParam("file"); - $media = Application_Model_StoredFile::RecallById($fileId); - if ($media != null) { - // 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->smartReadFile($media); - exit; - } else { - header ("HTTP/1.1 404 Not Found"); - } - } + $inline = !($this->_getParam('download',false) == true); + Application_Service_MediaService::streamFileDownload($fileId, $inline); $this->_helper->json->sendJson(array()); } - /** - * 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 - * @param string $mimeType - * @return void - * - * @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ - * @link http://php.net/manual/en/function.readfile.php#86244 - */ - public function smartReadFile($media) - { - $filepath = $media->getFilePath(); - $size= $media->getFileSize(); - $mimeType = $media->getPropelOrm()->getDbMime(); - - $fm = @fopen($filepath, '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'); - 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 public function onAirLightAction() diff --git a/airtime_mvc/application/controllers/AudiopreviewController.php b/airtime_mvc/application/controllers/AudiopreviewController.php index e90c34a39..60c01ba8c 100644 --- a/airtime_mvc/application/controllers/AudiopreviewController.php +++ b/airtime_mvc/application/controllers/AudiopreviewController.php @@ -22,8 +22,6 @@ class AudiopreviewController extends Zend_Controller_Action $CC_CONFIG = Config::getConfig(); $audioFileID = $this->_getParam('audioFileID'); - $audioFileArtist = $this->_getParam('audioFileArtist'); - $audioFileTitle = $this->_getParam('audioFileTitle'); $type = $this->_getParam('type'); $baseUrl = Application_Common_OsPath::getBaseDir(); @@ -60,10 +58,9 @@ class AudiopreviewController extends Zend_Controller_Action $this->view->uri = $uri; $this->view->mime = $mime; $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->audioFileArtist = htmlspecialchars($media->getPropelOrm()->getDbArtistName()); + $this->view->audioFileTitle = htmlspecialchars($media->getPropelOrm()->getDbTrackTitle()); $this->view->type = $type; $this->_helper->viewRenderer->setRender('audio-preview'); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 53f156b24..a40823a5b 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -947,7 +947,6 @@ SQL; $baseUrl = Application_Common_OsPath::getBaseDir(); $filesize = $file->getFileSize(); - self::createFileScheduleEvent($data, $item, $media_id, $uri, $filesize); } elseif (!is_null($item['stream_id'])) { diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 04a9e3b95..1e3a72dce 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -57,7 +57,7 @@ class CcFiles extends BaseCcFiles { * Retrieve a sanitized version of the file metadata, suitable for public access. * @param $fileId */ - public static function getSantiziedFileById($fileId) + public static function getSanitizedFileById($fileId) { $file = CcFilesQuery::create()->findPk($fileId); if ($file) { @@ -114,7 +114,7 @@ class CcFiles extends BaseCcFiles { $file->setDbHidden(true); $file->save(); - $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "/rest/media/" . $file->getPrimaryKey(); + $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "rest/media/" . $file->getPrimaryKey(); Application_Service_MediaService::processUploadedFile($callbackUrl, $relativePath, self::getOwnerId()); return CcFiles::sanitizeResponse($file); @@ -138,14 +138,11 @@ class CcFiles extends BaseCcFiles { { $file = CcFilesQuery::create()->findPk($fileId); - // Since we check for this value when deleting files, set it first - $file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); - $fileArray = self::removeBlacklistedFields($fileArray); $fileArray = self::stripTimeStampFromYearTag($fileArray); self::validateFileArray($fileArray); - if ($file && isset($requestData["resource_id"])) { + if ($file && isset($fileArray["resource_id"])) { $file->fromArray($fileArray, BasePeer::TYPE_FIELDNAME); @@ -155,9 +152,10 @@ class CcFiles extends BaseCcFiles { $fileSizeBytes = $fileArray["filesize"]; if (!isset($fileSizeBytes) || $fileSizeBytes === false) { - $file->setDbImportStatus(2)->save(); + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); throw new FileNotFoundException(); } + $cloudFile = new CloudFile(); $cloudFile->setStorageBackend($fileArray["storage_backend"]); $cloudFile->setResourceId($fileArray["resource_id"]); @@ -172,6 +170,9 @@ class CcFiles extends BaseCcFiles { } 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 @@ -252,6 +253,7 @@ class CcFiles extends BaseCcFiles { } } + private static function validateFileArray(&$fileArray) { // Sanitize any wildly incorrect metadata before it goes to be validated @@ -342,6 +344,42 @@ class CcFiles extends BaseCcFiles { return $response; } + /** + * Returns the file size in bytes. + */ + public function getFileSize() + { + return filesize($this->getAbsoluteFilePath()); + } + + public function getFilename() + { + $info = pathinfo($this->getAbsoluteFilePath()); + return $info['filename']; + } + + /** + * Returns the file's absolute file path stored on disk. + */ + public function getURLForTrackPreviewOrDownload() + { + return $this->getAbsoluteFilePath(); + } + + /** + * Returns the file's absolute file path stored on disk. + */ + public function getAbsoluteFilePath() + { + $music_dir = Application_Model_MusicDir::getDirByPK($this->getDbDirectory()); + if (!$music_dir) { + throw new Exception("Invalid music_dir for file in database."); + } + $directory = $music_dir->getDirectory(); + $filepath = $this->getDbFilepath(); + return Application_Common_OsPath::join($directory, $filepath); + } + /** * * Strips out fields from incoming request data that should never be modified @@ -421,43 +459,7 @@ class CcFiles extends BaseCcFiles { exec("find $path -empty -type d -delete"); } - /** - * Returns the file size in bytes. - */ - public function getFileSize() - { - return filesize($this->getAbsoluteFilePath()); - } - - public function getFilename() - { - $info = pathinfo($this->getAbsoluteFilePath()); - return $info['filename']; - } - - /** - * Returns the file's absolute file path stored on disk. - */ - public function getURLForTrackPreviewOrDownload() - { - return $this->getAbsoluteFilePath(); - } - - /** - * Returns the file's absolute file path stored on disk. - */ - public function getAbsoluteFilePath() - { - $music_dir = Application_Model_MusicDir::getDirByPK($this->getDbDirectory()); - if (!$music_dir) { - throw new Exception("Invalid music_dir for file in database."); - } - $directory = $music_dir->getDirectory(); - $filepath = $this->getDbFilepath(); - return Application_Common_OsPath::join($directory, $filepath); - } - /** * Checks if the file is a regular file that can be previewed and downloaded. */ diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index c6e26b6f9..0df6b6b3e 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -41,8 +41,9 @@ class Rest_MediaController extends Zend_Rest_Controller try { $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody($this->_redirect(CcFiles::getDownloadUrl($id))); + ->setHttpResponseCode(200); + $inline = false; + Application_Service_MediaService::streamFileDownload($id, $inline); } catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); @@ -64,7 +65,7 @@ class Rest_MediaController extends Zend_Rest_Controller try { $this->getResponse() ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::getSantiziedFileById($id))); + ->appendBody(json_encode(CcFiles::getSanitizedFileById($id))); } catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php index 74b2609d0..81f199ae7 100644 --- a/airtime_mvc/application/services/MediaService.php +++ b/airtime_mvc/application/services/MediaService.php @@ -1,5 +1,7 @@ getDirectory() . "/imported/" . $ownerId; + $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 @@ -34,8 +39,121 @@ class Application_Service_MediaService //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); + $callbackUrl, $apiKey, + $CC_CONFIG["current_backend"], + $storageBackend->getFilePrefix()); } -} \ No newline at end of file + + + /** + * @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(); + $media = Application_Model_StoredFile::RecallById($fileId); + if ($media == null) { + throw new FileNotFoundException(); + } + + // 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 . '"'); + } + + self::smartReadFile($media); + exit; + } else { + throw new FileNotFoundException(); + } + } + + + /** + * 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 CcFile $media + * @return void + * + * @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ + * @link http://php.net/manual/en/function.readfile.php#86244 + */ + private static function smartReadFile($media) + { + $filepath = $media->getFilePath(); + $size= $media->getFileSize(); + $mimeType = $media->getPropelOrm()->getDbMime(); + + $fm = @fopen($filepath, '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'); + 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); + } +} + diff --git a/airtime_mvc/public/js/airtime/common/common.js b/airtime_mvc/public/js/airtime/common/common.js index 7a043c286..476b5e77d 100644 --- a/airtime_mvc/public/js/airtime/common/common.js +++ b/airtime_mvc/public/js/airtime/common/common.js @@ -91,16 +91,11 @@ function openAudioPreview(p_event) { } } -function open_audio_preview(type, id, audioFileTitle, audioFileArtist) { - // we need to remove soundcloud icon from audioFileTitle - var index = audioFileTitle.indexOf(" Date: Fri, 20 Feb 2015 14:27:16 -0500 Subject: [PATCH 05/46] Merge of cc-5709-airtime-analyzer-saas-refactor into saas --- .../cloud_storage/ProxyStorageBackend.php | 14 +- airtime_mvc/application/common/HTTPHelper.php | 18 + airtime_mvc/application/configs/conf.php | 4 +- .../application/controllers/ApiController.php | 103 +---- .../controllers/AudiopreviewController.php | 9 +- airtime_mvc/application/models/Schedule.php | 1 - .../application/models/airtime/CcFiles.php | 378 ++++++++++++++++- .../rest/controllers/MediaController.php | 391 +++--------------- .../application/services/MediaService.php | 159 +++++++ airtime_mvc/public/index.php | 3 + .../public/js/airtime/common/common.js | 9 +- .../public/js/airtime/library/library.js | 6 +- 12 files changed, 627 insertions(+), 468 deletions(-) create mode 100644 airtime_mvc/application/services/MediaService.php diff --git a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php index 29f8a9caa..fa45da5f8 100644 --- a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php +++ b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php @@ -5,16 +5,16 @@ require_once 'FileStorageBackend.php'; require_once 'Amazon_S3StorageBackend.php'; /** - * + * * Controls access to the storage backend class where a file is stored. * */ class ProxyStorageBackend extends StorageBackend { private $storageBackend; - + /** - * Receives the file's storage backend and instantiates the approriate + * Receives the file's storage backend and instantiates the appropriate * object. */ public function ProxyStorageBackend($storageBackend) @@ -32,22 +32,22 @@ class ProxyStorageBackend extends StorageBackend $this->storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]); } } - + public function getAbsoluteFilePath($resourceId) { return $this->storageBackend->getAbsoluteFilePath($resourceId); } - + public function getSignedURL($resourceId) { return $this->storageBackend->getSignedURL($resourceId); } - + public function getFileSize($resourceId) { return $this->storageBackend->getFileSize($resourceId); } - + public function deletePhysicalFile($resourceId) { $this->storageBackend->deletePhysicalFile($resourceId); diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index db314bb0b..505befd7e 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -17,4 +17,22 @@ class Application_Common_HTTPHelper $request->getParam("timezone", null) ); } + + public static function getStationUrl() + { + $scheme = "http"; + if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $scheme = "https"; + } + $CC_CONFIG = Config::getConfig(); + $baseUrl = $CC_CONFIG['baseUrl']; + $baseDir = $CC_CONFIG['baseDir']; + $basePort = $CC_CONFIG['basePort']; + if (empty($baseDir)) { + $baseDir = "/"; + } + $stationUrl = "$scheme://${baseUrl}:${basePort}${baseDir}"; + + return $stationUrl; + } } diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index d38a03797..eae99d778 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -43,7 +43,7 @@ class Config { } else { $CC_CONFIG['dev_env'] = 'production'; } - + // Parse separate conf file for cloud storage values $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf"; if (!file_exists($cloudStorageConfig)) { @@ -92,7 +92,7 @@ class Config { public static function setAirtimeVersion() { $airtime_version = Application_Model_Preference::GetAirtimeVersion(); $uniqueid = Application_Model_Preference::GetUniqueId(); - $buildVersion = file_get_contents(self::$rootDir."/../VERSION"); + $buildVersion = @file_get_contents(self::$rootDir."/../VERSION"); self::$CC_CONFIG['airtime_version'] = md5($airtime_version.$buildVersion); } diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index 124b4b4d0..f40fa0e5e 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -95,32 +95,8 @@ class ApiController extends Zend_Controller_Action $fileId = $this->_getParam("file"); - $media = Application_Model_StoredFile::RecallById($fileId); - if ($media != null) { - // 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"); - } - } + $inline = !($this->_getParam('download',false) == true); + Application_Service_MediaService::streamFileDownload($fileId, $inline); $this->_helper->json->sendJson(array()); } @@ -137,82 +113,7 @@ class ApiController extends Zend_Controller_Action $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 (!$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'); - 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 public function onAirLightAction() diff --git a/airtime_mvc/application/controllers/AudiopreviewController.php b/airtime_mvc/application/controllers/AudiopreviewController.php index a4e94b85c..d98610b6e 100644 --- a/airtime_mvc/application/controllers/AudiopreviewController.php +++ b/airtime_mvc/application/controllers/AudiopreviewController.php @@ -22,8 +22,6 @@ class AudiopreviewController extends Zend_Controller_Action $CC_CONFIG = Config::getConfig(); $audioFileID = $this->_getParam('audioFileID'); - $audioFileArtist = $this->_getParam('audioFileArtist'); - $audioFileTitle = $this->_getParam('audioFileTitle'); $type = $this->_getParam('type'); $baseUrl = Application_Common_OsPath::getBaseDir(); @@ -60,10 +58,9 @@ class AudiopreviewController extends Zend_Controller_Action $this->view->uri = $uri; $this->view->mime = $mime; $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->audioFileArtist = htmlspecialchars($media->getPropelOrm()->getDbArtistName()); + $this->view->audioFileTitle = htmlspecialchars($media->getPropelOrm()->getDbTrackTitle()); $this->view->type = $type; $this->_helper->viewRenderer->setRender('audio-preview'); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index a893d9dbe..13d53df55 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -954,7 +954,6 @@ SQL; $baseUrl = Application_Common_OsPath::getBaseDir(); $filesize = $file->getFileSize(); - self::createFileScheduleEvent($data, $item, $media_id, $uri, $filesize); } diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 3bd62a7a5..1e3a72dce 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -11,17 +11,290 @@ * * @package propel.generator.campcaster */ + +class InvalidMetadataException extends Exception +{ +} + +class FileNotFoundException extends Exception +{ +} + +class OverDiskQuotaException extends Exception +{ + +} + class CcFiles extends BaseCcFiles { - + + const MUSIC_DIRS_STOR_PK = 1; + + + //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 private static $privateFields = array( - 'file_exists', - 'silan_check', - 'is_scheduled', - 'is_playlist' + 'file_exists', + 'silan_check', + 'is_scheduled', + '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(); + } + + $file = new CcFiles(); + + try{ + $fileArray = self::removeBlacklistedFields($fileArray); + + /*if (!self::validateFileArray($fileArray)) + { + $file->setDbTrackTitle($_FILES["file"]["name"]); + $file->setDbUtime(new DateTime("now", new DateTimeZone("UTC"))); + $file->save(); + return CcFiles::sanitizeResponse($file);*/ + self::validateFileArray($fileArray); + + /* 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($fileArray["full_path"])) { + $fullPath = $fileArray["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($fileArray); + $file->setDbOwnerId(self::getOwnerId()); + $now = new DateTime("now", new DateTimeZone("UTC")); + $file->setDbTrackTitle($_FILES["file"]["name"]); + $file->setDbUtime($now); + $file->setDbHidden(true); + $file->save(); + + $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "rest/media/" . $file->getPrimaryKey(); + + Application_Service_MediaService::processUploadedFile($callbackUrl, $relativePath, self::getOwnerId()); + return CcFiles::sanitizeResponse($file); + + } catch (Exception $e) { + $file->setDbImportStatus(2); + $file->setDbHidden(true); + throw $e; + } + } + + /** Update a file with metadata specified in an array. + * @param $fileId The ID of the file to update in the DB. + * @param $fileArray 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); + + 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) + { + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + throw new FileNotFoundException(); + } + + $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) + { + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + throw new FileNotFoundException(); + } + 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(); + + /* $this->removeEmptySubFolders( + isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."/srv/airtime/stor/organize/" : "/srv/airtime/stor/organize/"); */ + } else { + $file->setDbImportStatus(self::IMPORT_STATUS_FAILED)->save(); + throw new FileNotFoundException(); + } + + 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 = new Application_Model_StoredFile($file, $con); + if ($storedFile->existsOnDisk()) { + $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? + } + $file->delete(); + } else { + throw new FileNotFoundException(); + } + + } + + public static function getDownloadUrl($id) + { + $file = CcFilesQuery::create()->findPk($id); + if ($file) { + $con = Propel::getConnection(); + $storedFile = new Application_Model_StoredFile($file, $con); + $baseDir = Application_Common_OsPath::getBaseDir(); + + return $storedFile->getRelativeFileUrl($baseDir) . '/download/true'; + } + 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(); $cueout = $this->getDbCueout(); @@ -70,7 +343,7 @@ class CcFiles extends BaseCcFiles { return $response; } - + /** * Returns the file size in bytes. */ @@ -78,13 +351,13 @@ class CcFiles extends BaseCcFiles { { return filesize($this->getAbsoluteFilePath()); } - + public function getFilename() { $info = pathinfo($this->getAbsoluteFilePath()); return $info['filename']; } - + /** * Returns the file's absolute file path stored on disk. */ @@ -92,7 +365,7 @@ class CcFiles extends BaseCcFiles { { return $this->getAbsoluteFilePath(); } - + /** * Returns the file's absolute file path stored on disk. */ @@ -104,10 +377,89 @@ class CcFiles extends BaseCcFiles { } $directory = $music_dir->getDirectory(); $filepath = $this->getDbFilepath(); - 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. */ diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index cbe53ac30..02e7fc503 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -4,26 +4,10 @@ require_once 'ProxyStorageBackend.php'; 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() { @@ -59,18 +43,20 @@ class Rest_MediaController extends Zend_Rest_Controller return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - $con = Propel::getConnection(); - $storedFile = new Application_Model_StoredFile($file, $con); - $baseUrl = Application_Common_OsPath::getBaseDir(); - - $CC_CONFIG = Config::getConfig(); + try + { $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody($this->_redirect($storedFile->getRelativeFileUrl($baseUrl).'/download/true/api_key/'.$CC_CONFIG["apiKey"][0])); - } else { + ->setHttpResponseCode(200); + $inline = false; + Application_Service_MediaService::streamFileDownload($id, $inline); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -81,14 +67,18 @@ class Rest_MediaController extends Zend_Rest_Controller return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - + try { $this->getResponse() ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::sanitizeResponse($file))); - } else { + ->appendBody(json_encode(CcFiles::getSanitizedFileById($id))); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -103,52 +93,24 @@ class Rest_MediaController extends Zend_Rest_Controller 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() ->setHttpResponseCode(400) ->appendBody("ERROR: Disk Quota reached."); - return; } - - $file = new CcFiles(); - $whiteList = $this->removeBlacklistedFieldsFromRequestData($this->getRequest()->getPost()); - - 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))); + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -158,89 +120,25 @@ class Rest_MediaController extends Zend_Rest_Controller if (!$id) { return; } - - $file = CcFilesQuery::create()->findPk($id); - // Since we check for this value when deleting files, set it first - //$file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); - - $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(); - + try { + $requestData = json_decode($this->getRequest()->getRawBody(), true); + $sanitizedFile = CcFiles::updateFromArray($id, $requestData); $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody(json_encode(CcFiles::sanitizeResponse($file))); - } else if ($file) { - - //local file storage - $file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); - - $file->fromArray($whiteList, 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($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(); + ->setHttpResponseCode(201) + ->appendBody(json_encode($sanitizedFile)); + } + catch (InvalidMetadataException $e) { + $this->invalidDataResponse(); + Logging::error($e->getMessage()); + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -250,16 +148,18 @@ class Rest_MediaController extends Zend_Rest_Controller if (!$id) { return; } - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - $con = Propel::getConnection(); - $storedFile = Application_Model_StoredFile::RecallById($id, $con); - $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? - + try { + CcFiles::deleteById($id); $this->getResponse() ->setHttpResponseCode(204); - } else { + } + catch (FileNotFoundException $e) { $this->fileNotFoundResponse(); + Logging::error($e->getMessage()); + } + catch (Exception $e) { + $this->unknownErrorResponse(); + Logging::error($e->getMessage()); } } @@ -288,177 +188,12 @@ class Rest_MediaController extends Zend_Rest_Controller $resp->appendBody("ERROR: Import Failed."); } - private function invalidDataResponse() + private function unknownErrorResponse() { $resp = $this->getResponse(); - $resp->setHttpResponseCode(422); - $resp->appendBody("ERROR: Invalid data"); - } - 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; + $resp->setHttpResponseCode(400); + $resp->appendBody("An unknown error occurred."); } } diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php new file mode 100644 index 000000000..81f199ae7 --- /dev/null +++ b/airtime_mvc/application/services/MediaService.php @@ -0,0 +1,159 @@ +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()); + } + + + /** + * @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(); + $media = Application_Model_StoredFile::RecallById($fileId); + if ($media == null) { + throw new FileNotFoundException(); + } + + // 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 . '"'); + } + + self::smartReadFile($media); + exit; + } else { + throw new FileNotFoundException(); + } + } + + + /** + * 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 CcFile $media + * @return void + * + * @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ + * @link http://php.net/manual/en/function.readfile.php#86244 + */ + private static function smartReadFile($media) + { + $filepath = $media->getFilePath(); + $size= $media->getFileSize(); + $mimeType = $media->getPropelOrm()->getDbMime(); + + $fm = @fopen($filepath, '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'); + 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); + } +} + diff --git a/airtime_mvc/public/index.php b/airtime_mvc/public/index.php index f9c6e4fe4..67c659008 100644 --- a/airtime_mvc/public/index.php +++ b/airtime_mvc/public/index.php @@ -54,6 +54,9 @@ set_include_path(APPLICATION_PATH . '/controllers' . PATH_SEPARATOR . get_includ //Controller plugins. 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 if (file_exists('/usr/share/php/libzend-framework-php')) { set_include_path('/usr/share/php/libzend-framework-php' . PATH_SEPARATOR . get_include_path()); diff --git a/airtime_mvc/public/js/airtime/common/common.js b/airtime_mvc/public/js/airtime/common/common.js index 7a043c286..476b5e77d 100644 --- a/airtime_mvc/public/js/airtime/common/common.js +++ b/airtime_mvc/public/js/airtime/common/common.js @@ -91,16 +91,11 @@ function openAudioPreview(p_event) { } } -function open_audio_preview(type, id, audioFileTitle, audioFileArtist) { - // we need to remove soundcloud icon from audioFileTitle - var index = audioFileTitle.indexOf(" */ - public static function copyFileToStor($tempFilePath, $originalFilename) + public static function moveFileToStor($tempFilePath, $originalFilename, $copyFile=false) { $audio_file = $tempFilePath; - Logging::info('copyFileToStor: moving file '.$audio_file); - + $storDir = Application_Model_MusicDir::getStorDir(); $stor = $storDir->getDirectory(); // check if "organize" dir exists and if not create one @@ -966,55 +964,35 @@ SQL; 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 $audio_stor = Application_Common_OsPath::join($stor, "organize", $originalFilename); - $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 //encoding is ISO-8859-1 $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"); - // Martin K.: changed to rename: Much less load + quicker since this is - // an atomic operation - 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 " - ."directory does not have correct write permissions."); + if ($copyFile) { + Logging::info("Copying file $audio_file to $audio_stor"); + if (@copy($audio_file, $audio_stor) === false) { + throw new Exception("Failed to copy $audio_file to $audio_stor"); + } + } else { + Logging::info("Moving file $audio_file to $audio_stor"); + + // Martin K.: changed to rename: Much less load + quicker since this is + // an atomic operation + 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 " + . "directory does not have correct write permissions."); + } } return $audio_stor; } diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 1e3a72dce..8463e2542 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -29,6 +29,10 @@ 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( @@ -78,57 +82,104 @@ class CcFiles extends BaseCcFiles { throw new OverDiskQuotaException(); } - $file = new CcFiles(); + /* 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. + */ - try{ - $fileArray = self::removeBlacklistedFields($fileArray); + //Extract the relative path to the temporary uploaded file on disk. + if (isset($fileArray["full_path"])) { + $fullPath = $fileArray["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 + $filePathRelativeToOrganize = substr($fullPath, strlen($basePath)-1); + $originalFilename = $filePathRelativeToOrganize; + } else { + //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"]; + } - /*if (!self::validateFileArray($fileArray)) - { - $file->setDbTrackTitle($_FILES["file"]["name"]); - $file->setDbUtime(new DateTime("now", new DateTimeZone("UTC"))); - $file->save(); - return CcFiles::sanitizeResponse($file);*/ - self::validateFileArray($fileArray); + $tempFilePath = $_FILES['file']['tmp_name']; - /* 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($fileArray["full_path"])) { - $fullPath = $fileArray["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($fileArray); - $file->setDbOwnerId(self::getOwnerId()); - $now = new DateTime("now", new DateTimeZone("UTC")); - $file->setDbTrackTitle($_FILES["file"]["name"]); - $file->setDbUtime($now); - $file->setDbHidden(true); - $file->save(); - - $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "rest/media/" . $file->getPrimaryKey(); - - Application_Service_MediaService::processUploadedFile($callbackUrl, $relativePath, self::getOwnerId()); - return CcFiles::sanitizeResponse($file); - - } catch (Exception $e) { - $file->setDbImportStatus(2); - $file->setDbHidden(true); + 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($filePath, $copyFile=false) + { + $fileArray = array(); + $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 The ID of the file to update in the DB. - * @param $fileArray An associative array containing metadata. Replaces those fields if they exist. + * @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 @@ -238,22 +289,6 @@ class CcFiles extends BaseCcFiles { } - public static function getDownloadUrl($id) - { - $file = CcFilesQuery::create()->findPk($id); - if ($file) { - $con = Propel::getConnection(); - $storedFile = new Application_Model_StoredFile($file, $con); - $baseDir = Application_Common_OsPath::getBaseDir(); - - return $storedFile->getRelativeFileUrl($baseDir) . '/download/true'; - } - else { - throw new FileNotFoundException(); - } - } - - private static function validateFileArray(&$fileArray) { // Sanitize any wildly incorrect metadata before it goes to be validated @@ -330,7 +365,7 @@ class CcFiles extends BaseCcFiles { /** * * 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? public static function sanitizeResponse($file) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 0df6b6b3e..1735751dc 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -2,10 +2,6 @@ class Rest_MediaController extends Zend_Rest_Controller { - const IMPORT_STATUS_SUCCESS = 0; - const IMPORT_STATUS_PENDING = 1; - const IMPORT_STATUS_FAILED = 2; - public function init() { diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php index 81f199ae7..037b88de2 100644 --- a/airtime_mvc/application/services/MediaService.php +++ b/airtime_mvc/application/services/MediaService.php @@ -4,38 +4,30 @@ require_once('ProxyStorageBackend.php'); class Application_Service_MediaService { - public static function processUploadedFile($callbackUrl, $originalFilename, $ownerId) + /** 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]; - $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; - } + //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! @@ -45,6 +37,8 @@ class Application_Service_MediaService $callbackUrl, $apiKey, $CC_CONFIG["current_backend"], $storageBackend->getFilePrefix()); + + return $newTempFilePath; } From ce36c1be18ce924515399c561f19aef4f0325b49 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Fri, 20 Feb 2015 16:58:09 -0500 Subject: [PATCH 07/46] Removed unnecessary call to BaseCcFiles->delete --- airtime_mvc/application/models/airtime/CcFiles.php | 1 - 1 file changed, 1 deletion(-) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 8463e2542..e27bed4a9 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -282,7 +282,6 @@ class CcFiles extends BaseCcFiles { if ($storedFile->existsOnDisk()) { $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? } - $file->delete(); } else { throw new FileNotFoundException(); } From 909cbae5f4b1a95339b76f7d64d7dd0a8ea86304 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Fri, 20 Feb 2015 17:34:58 -0500 Subject: [PATCH 08/46] Added new parameter to CcFiles::createFromLocalFile() for flexibility --- airtime_mvc/application/models/airtime/CcFiles.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index e27bed4a9..eb50f8821 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -119,9 +119,8 @@ class CcFiles extends BaseCcFiles { * @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($filePath, $copyFile=false) + public static function createFromLocalFile($fileArray, $filePath, $copyFile=false) { - $fileArray = array(); $info = pathinfo($filePath); $fileName = basename($filePath).'.'.$info['extension']; self::createAndImport($fileArray, $filePath, $fileName, $copyFile); From c868136d26b916c5b8fdeec3a187800262cbef47 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Fri, 20 Feb 2015 18:21:49 -0500 Subject: [PATCH 09/46] Make airtime_analyzer respect the storage_backend setting passed to it by Airtime --- airtime_mvc/application/models/RabbitMq.php | 4 ++-- .../airtime_analyzer/analyzer_pipeline.py | 6 +++--- .../airtime_analyzer/message_listener.py | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index 6918abc06..2ff57d2ed 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -80,12 +80,12 @@ class Application_Model_RabbitMq } public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename, - $callbackUrl, $apiKey, $currentStorageBackend) + $callbackUrl, $apiKey, $storageBackend) { $exchange = 'airtime-uploads'; $data['tmp_file_path'] = $tmpFilePath; - $data['current_storage_backend'] = $currentStorageBackend; + $data['storage_backend'] = $storageBackend; $data['import_directory'] = $importedStorageDirectory; $data['original_filename'] = $originalFilename; $data['callback_url'] = $callbackUrl; diff --git a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py index f3cf04180..f25994921 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py @@ -21,7 +21,7 @@ class AnalyzerPipeline: """ @staticmethod - def run_analysis(queue, audio_file_path, import_directory, original_filename, cloud_storage_enabled): + def run_analysis(queue, audio_file_path, import_directory, original_filename, storage_backend): """Analyze and import an audio file, and put all extracted metadata into queue. Keyword arguments: @@ -34,7 +34,7 @@ class AnalyzerPipeline: preserve. The file at audio_file_path typically has a temporary randomly generated name, which is why we want to know what the original name was. - cloud_storage_enabled: Whether to store the file in the cloud or on the local disk. + storage_backend: String indicating the storage backend (amazon_s3 or file) """ # It is super critical to initialize a separate log file here so that we # don't inherit logging/locks from the parent process. Supposedly @@ -62,7 +62,7 @@ class AnalyzerPipeline: metadata = ReplayGainAnalyzer.analyze(audio_file_path, metadata) metadata = PlayabilityAnalyzer.analyze(audio_file_path, metadata) - if cloud_storage_enabled: + if storage_backend.lower() == "amazon_S3": csu = CloudStorageUploader() metadata = csu.upload_obj(audio_file_path, metadata) else: diff --git a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py index 8ed5fa782..a304929dc 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py @@ -165,8 +165,9 @@ class MessageListener: audio_file_path = msg_dict["tmp_file_path"] import_directory = msg_dict["import_directory"] original_filename = msg_dict["original_filename"] + storage_backend = msg_dict["storage_backend"] - audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename) + audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename, storage_backend) StatusReporter.report_success_to_callback_url(callback_url, api_key, audio_metadata) except KeyError as e: @@ -205,15 +206,12 @@ class MessageListener: channel.basic_ack(delivery_tag=method_frame.delivery_tag) @staticmethod - def spawn_analyzer_process(audio_file_path, import_directory, original_filename): + def spawn_analyzer_process(audio_file_path, import_directory, original_filename, storage_backend): ''' Spawn a child process to analyze and import a new audio file. ''' - csu = CloudStorageUploader() - cloud_storage_enabled = csu.enabled() - q = multiprocessing.Queue() p = multiprocessing.Process(target=AnalyzerPipeline.run_analysis, - args=(q, audio_file_path, import_directory, original_filename, cloud_storage_enabled)) + args=(q, audio_file_path, import_directory, original_filename, storage_backend)) p.start() p.join() if p.exitcode == 0: From 73e5fb938f7a5afa74eabe57072d1d5c5765e32f Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Tue, 24 Feb 2015 11:13:39 -0500 Subject: [PATCH 10/46] Use more secure random number generation for CSRF auth tokens * Also cleaned up pull request --- airtime_mvc/application/controllers/PluploadController.php | 6 +++--- airtime_mvc/application/controllers/plugins/Acl_plugin.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/controllers/PluploadController.php b/airtime_mvc/application/controllers/PluploadController.php index 6aae736ae..7c808140c 100644 --- a/airtime_mvc/application/controllers/PluploadController.php +++ b/airtime_mvc/application/controllers/PluploadController.php @@ -31,10 +31,10 @@ class PluploadController extends Zend_Controller_Action $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'); - /* Moved to be globally set in Bootstrap */ - // $csrf_namespace->setExpirationSeconds(5*60*60); - // $csrf_namespace->authtoken = sha1(uniqid(rand(),1)); + //The CSRF token is generated in Bootstrap.php $csrf_element = new Zend_Form_Element_Hidden('csrf'); $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); diff --git a/airtime_mvc/application/controllers/plugins/Acl_plugin.php b/airtime_mvc/application/controllers/plugins/Acl_plugin.php index fbf783131..d4577e471 100644 --- a/airtime_mvc/application/controllers/plugins/Acl_plugin.php +++ b/airtime_mvc/application/controllers/plugins/Acl_plugin.php @@ -156,7 +156,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract if (!$tokenValid) { $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); - $csrf_namespace->authtoken = sha1(uniqid(rand(),1)); + $csrf_namespace->authtoken = sha1(openssl_random_pseudo_bytes(128)); Logging::warn("Invalid CSRF token: $token"); $this->getResponse() From a52e2faeb94254cdddd57b58cb6b7e8f04f43be9 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Tue, 24 Feb 2015 13:10:20 -0500 Subject: [PATCH 11/46] SAAS-602: Fix getaddrinfo deadlock (again) --- .../airtime_analyzer/cloud_storage_uploader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py index b8fc2261f..11eb68b19 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py @@ -5,6 +5,10 @@ import ConfigParser from boto.s3.connection import S3Connection from boto.s3.key import Key +# Fix for getaddrinfo deadlock. See these issues for details: +# https://github.com/gevent/gevent/issues/349 +# https://github.com/docker/docker-registry/issues/400 +u'fix getaddrinfo deadlock'.encode('idna') CLOUD_CONFIG_PATH = '/etc/airtime-saas/cloud_storage.conf' STORAGE_BACKEND_FILE = "file" From e0a050ac608a48c470c3463a7333dd568bd1109b Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 25 Feb 2015 15:09:02 -0500 Subject: [PATCH 12/46] Fix file deletion bug --- airtime_mvc/application/models/StoredFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 26cbea647..f7c023bb4 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -392,7 +392,7 @@ SQL; Logging::info("User ".$user->getLogin()." is deleting file: ".$this->_file->getDbTrackTitle()." - file id: ".$file_id); $filesize = $this->_file->getFileSize(); - if ($filesize <= 0) { + if ($filesize < 0) { throw new Exception("Cannot delete file with filesize ".$filesize); } From 7e167e5a47e1d695aeb7a2f599651cb831223a42 Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 25 Feb 2015 15:28:48 -0500 Subject: [PATCH 13/46] Temporarily disabling disk quota limit until all file sizes have been set --- airtime_mvc/application/controllers/PluploadController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/controllers/PluploadController.php b/airtime_mvc/application/controllers/PluploadController.php index f1197ec2b..16ce257db 100644 --- a/airtime_mvc/application/controllers/PluploadController.php +++ b/airtime_mvc/application/controllers/PluploadController.php @@ -27,9 +27,10 @@ class PluploadController extends Zend_Controller_Action $this->view->headLink()->appendStylesheet($baseUrl.'css/addmedia.css?'.$CC_CONFIG['airtime_version']); $this->view->quotaLimitReached = false; - if (Application_Model_Systemstatus::isDiskOverQuota()) { + // temporarily disabling disk quota until all file size values have been set + /*if (Application_Model_Systemstatus::isDiskOverQuota()) { $this->view->quotaLimitReached = true; - } + }*/ $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); $csrf_namespace->setExpirationSeconds(5*60*60); From 5acad059decdbe112fac9692cacacf3ab718f9c7 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 25 Feb 2015 16:09:08 -0500 Subject: [PATCH 14/46] Insert leading slash into basedir for station URL --- airtime_mvc/application/common/HTTPHelper.php | 3 +++ airtime_mvc/application/models/airtime/CcFiles.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index 505befd7e..2b6b24cee 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -31,6 +31,9 @@ class Application_Common_HTTPHelper if (empty($baseDir)) { $baseDir = "/"; } + if ($baseDir[0] != "") { + $baseDir = "/" . $baseDir; + } $stationUrl = "$scheme://${baseUrl}:${basePort}${baseDir}"; return $stationUrl; diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 0d59e576a..079b47556 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -160,7 +160,7 @@ class CcFiles extends BaseCcFiles { throw new Exception("Bad file extension."); } - $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "rest/media/" . $file->getPrimaryKey(); + $callbackUrl = Application_Common_HTTPHelper::getStationUrl() . "/rest/media/" . $file->getPrimaryKey(); Application_Service_MediaService::importFileToLibrary($callbackUrl, $filePath, $originalFilename, self::getOwnerId(), $copyFile); From 48b4e3fb623398524622f38dee851e1fc3c97676 Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 25 Feb 2015 16:11:52 -0500 Subject: [PATCH 15/46] Also need to diable disk usage in media controller --- .../application/modules/rest/controllers/MediaController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index cbe53ac30..1c18c233d 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -103,12 +103,13 @@ class Rest_MediaController extends Zend_Rest_Controller return; } - if (Application_Model_Systemstatus::isDiskOverQuota()) { + //temporarily disabling disk quota until all file sizes have ben set in the database. + /*if (Application_Model_Systemstatus::isDiskOverQuota()) { $this->getResponse() ->setHttpResponseCode(400) ->appendBody("ERROR: Disk Quota reached."); return; - } + }*/ $file = new CcFiles(); $whiteList = $this->removeBlacklistedFieldsFromRequestData($this->getRequest()->getPost()); From 7d534d1cda90f2058ce57440beb8b0a6e6d399f3 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 25 Feb 2015 16:19:01 -0500 Subject: [PATCH 16/46] Airtime Pro SSL compatibility hack --- airtime_mvc/application/common/HTTPHelper.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index 2b6b24cee..177892e1f 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -20,10 +20,6 @@ class Application_Common_HTTPHelper public static function getStationUrl() { - $scheme = "http"; - if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { - $scheme = "https"; - } $CC_CONFIG = Config::getConfig(); $baseUrl = $CC_CONFIG['baseUrl']; $baseDir = $CC_CONFIG['baseDir']; @@ -34,6 +30,13 @@ class Application_Common_HTTPHelper 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; From 3885abbdb80000494774aca37ac1cfe9ce561773 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 11:09:43 -0500 Subject: [PATCH 17/46] Fixed webstream previewing --- .../application/controllers/AudiopreviewController.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/controllers/AudiopreviewController.php b/airtime_mvc/application/controllers/AudiopreviewController.php index d98610b6e..c5ad22a58 100644 --- a/airtime_mvc/application/controllers/AudiopreviewController.php +++ b/airtime_mvc/application/controllers/AudiopreviewController.php @@ -47,10 +47,15 @@ class AudiopreviewController extends Zend_Controller_Action $media = Application_Model_StoredFile::RecallById($audioFileID); $uri = $baseUrl."api/get-media/file/".$audioFileID; $mime = $media->getPropelOrm()->getDbMime(); + $this->view->audioFileArtist = htmlspecialchars($media->getPropelOrm()->getDbArtistName()); + $this->view->audioFileTitle = htmlspecialchars($media->getPropelOrm()->getDbTrackTitle()); + } elseif ($type == "stream") { $webstream = CcWebstreamQuery::create()->findPk($audioFileID); $uri = $webstream->getDbUrl(); $mime = $webstream->getDbMime(); + $this->view->audioFileTitle = htmlspecialchars($webstream->getDbName()); + } else { throw new Exception("Unknown type for audio preview!.Type=$type"); } @@ -59,8 +64,6 @@ class AudiopreviewController extends Zend_Controller_Action $this->view->mime = $mime; $this->view->audioFileID = $audioFileID; - $this->view->audioFileArtist = htmlspecialchars($media->getPropelOrm()->getDbArtistName()); - $this->view->audioFileTitle = htmlspecialchars($media->getPropelOrm()->getDbTrackTitle()); $this->view->type = $type; $this->_helper->viewRenderer->setRender('audio-preview'); From bfd6ef72b18f19b82c0ff94ed6d3800420c90f2a Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 11:29:08 -0500 Subject: [PATCH 18/46] Fixed download URLs for non-cloud files --- airtime_mvc/application/controllers/LibraryController.php | 2 +- airtime_mvc/application/models/airtime/CcFiles.php | 3 ++- airtime_mvc/application/services/MediaService.php | 8 -------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 3e7d33cd7..8c635baa6 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -217,7 +217,7 @@ class LibraryController extends Zend_Controller_Action // and not the cloud_file id (if applicable) for track download. // Our application logic (StoredFile.php) will determine if the track // 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); } elseif ($type === "playlist" || $type === "block") { if ($type === 'playlist') { diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 079b47556..148eb37c0 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -388,7 +388,8 @@ class CcFiles extends BaseCcFiles { public function getFilename() { $info = pathinfo($this->getAbsoluteFilePath()); - return $info['filename']; + //filename doesn't contain the extension because PHP is awful + return $info['filename'].".".$info['extension']; } /** diff --git a/airtime_mvc/application/services/MediaService.php b/airtime_mvc/application/services/MediaService.php index eb0ad339a..ee4238e8d 100644 --- a/airtime_mvc/application/services/MediaService.php +++ b/airtime_mvc/application/services/MediaService.php @@ -56,19 +56,11 @@ class Application_Service_MediaService throw new FileNotFoundException(); } $filepath = $media->getFilePath(); - // Make sure we don't have some wrong result beecause of caching - clearstatcache(); - $media = Application_Model_StoredFile::RecallById($fileId); - if ($media == null) { - throw new FileNotFoundException(); - } - // 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 From edd07c18ae6eefd06801c85d1e07ee07a5207d0e Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 12:02:02 -0500 Subject: [PATCH 19/46] Fixed invalid detection of S3 backend in analyzer --- .../airtime_analyzer/airtime_analyzer/analyzer_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py index bfa053762..1247bfea9 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py @@ -69,7 +69,7 @@ class AnalyzerPipeline: metadata = ReplayGainAnalyzer.analyze(audio_file_path, metadata) metadata = PlayabilityAnalyzer.analyze(audio_file_path, metadata) - if storage_backend.lower() == "amazon_S3": + if storage_backend.lower() == u"amazon_s3": csu = CloudStorageUploader(cloud_storage_config) metadata = csu.upload_obj(audio_file_path, metadata) else: From 93f62eaadd485888234cb416d16c37029c3095d1 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Thu, 26 Feb 2015 12:49:52 -0500 Subject: [PATCH 20/46] Added check to hide LiveChat for existing hobbyist users --- airtime_mvc/application/Bootstrap.php | 8 +++++--- airtime_mvc/application/configs/conf.php | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 63fd52dde..153ce672d 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -88,7 +88,8 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap * from a php init function. This will save us from having to * 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 USER_MANUAL_URL = '" . USER_MANUAL_URL . "';"); $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';"); @@ -174,9 +175,10 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap && strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/audio-preview') === false && strpos($_SERVER['REQUEST_URI'], $baseUrl.'audiopreview/playlist-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()); - $plan_level = strval(Application_Model_Preference::GetPlanLevel()); $station_url = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; $view->headScript()->appendScript("var livechat_client_id = '$client_id';\n". "var livechat_plan_type = '$plan_level';\n". diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index d38a03797..6cf5f9d6c 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -92,7 +92,7 @@ class Config { public static function setAirtimeVersion() { $airtime_version = Application_Model_Preference::GetAirtimeVersion(); $uniqueid = Application_Model_Preference::GetUniqueId(); - $buildVersion = file_get_contents(self::$rootDir."/../VERSION"); + $buildVersion = @file_get_contents(self::$rootDir."/../VERSION"); self::$CC_CONFIG['airtime_version'] = md5($airtime_version.$buildVersion); } From d82f461f3580c7fb0a2f139912e00eae6289955e Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 13:06:29 -0500 Subject: [PATCH 21/46] Fix missing filesize and md5 for WAVE imports --- .../airtime_analyzer/metadata_analyzer.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py index 57d278548..58fc1d56e 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py @@ -28,6 +28,18 @@ class MetadataAnalyzer(Analyzer): #Other fields we'll want to set for Airtime: 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 mime_check = magic.from_file(filename, mime=True) 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... 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 find the mutagen_to_airtime_mapping = { From c57d4e3fc4c829ed2a5ac3bca8c4bc3cd5515527 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Thu, 26 Feb 2015 13:30:35 -0500 Subject: [PATCH 22/46] Check if station is on a trial plan before displaying the trial box --- .../application/views/scripts/partialviews/trialBox.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airtime_mvc/application/views/scripts/partialviews/trialBox.phtml b/airtime_mvc/application/views/scripts/partialviews/trialBox.phtml index b38973dc2..70f8ba322 100644 --- a/airtime_mvc/application/views/scripts/partialviews/trialBox.phtml +++ b/airtime_mvc/application/views/scripts/partialviews/trialBox.phtml @@ -1,4 +1,4 @@ -trial_remain != '' && $this->trial_remain != "Trial expired."){?> +is_trial && $this->trial_remain != '' && $this->trial_remain != "Trial expired."){?>

From 0b1af01822c80d494037c1d1a53632a5b249c576 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 13:52:51 -0500 Subject: [PATCH 23/46] Better exception logging and small pypo exception fix --- airtime_mvc/application/models/airtime/CcFiles.php | 2 +- python_apps/pypo/pypofile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 2ca2fac46..15bb3061e 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -100,7 +100,7 @@ class CcFiles extends BaseCcFiles { { $music_dir = Application_Model_MusicDir::getDirByPK($this->getDbDirectory()); 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(); $filepath = $this->getDbFilepath(); diff --git a/python_apps/pypo/pypofile.py b/python_apps/pypo/pypofile.py index 829c3fe23..3e5a400e5 100644 --- a/python_apps/pypo/pypofile.py +++ b/python_apps/pypo/pypofile.py @@ -167,7 +167,7 @@ class PypoFile(Thread): except IOError as e: logging.debug("Failed to open config file at %s: %s" % (config_path, e.strerror)) sys.exit() - except Exception: + except Exception as e: logging.debug(e.strerror) sys.exit() From 6047670dbdfe78f8573d85b62d83e44e9130fda8 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 14:12:51 -0500 Subject: [PATCH 24/46] Fix corner case where failed uploads could show up in the library --- .../modules/rest/controllers/MediaController.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 1c18c233d..4fd98706b 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -175,14 +175,16 @@ class Rest_MediaController extends Zend_Rest_Controller } 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(); + $file->setDbImportStatus(2); + $file->setHidden(true); + $file->save(); $this->fileNotFoundResponse(); return; } From ac3ad3d23bde3fceacf287237a22b3ee5f02d450 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 15:33:35 -0500 Subject: [PATCH 25/46] Prevent overwriting the database if it already exists --- airtime_mvc/application/common/ProvisioningHelper.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php index 9804e7d6e..9cc69ab5e 100644 --- a/airtime_mvc/application/common/ProvisioningHelper.php +++ b/airtime_mvc/application/common/ProvisioningHelper.php @@ -36,10 +36,13 @@ class ProvisioningHelper $this->parsePostParams(); //For security, the Airtime Pro provisioning system creates the database for the user. - // $this->setNewDatabaseConnection(); - //if ($this->checkDatabaseExists()) { - // throw new Exception("ERROR: Airtime database already exists"); - //} + $this->setNewDatabaseConnection(); + + //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->checkDatabaseExists()) { + throw new Exception("ERROR: Airtime database already exists"); + } //$this->createDatabase(); //All we need to do is create the database tables. From 2e2f1f976234b2ae3578ced67e55428f2cb1837f Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 26 Feb 2015 17:14:46 -0500 Subject: [PATCH 26/46] Fixed SAAS-612: Recreating a station can wipe its database --- .../application/common/ProvisioningHelper.php | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php index 9cc69ab5e..48bbddc8c 100644 --- a/airtime_mvc/application/common/ProvisioningHelper.php +++ b/airtime_mvc/application/common/ProvisioningHelper.php @@ -38,20 +38,31 @@ class ProvisioningHelper //For security, the Airtime Pro provisioning system creates the database for the user. $this->setNewDatabaseConnection(); + //if ($this->checkDatabaseExists()) { + // 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->checkDatabaseExists()) { - throw new Exception("ERROR: Airtime database already exists"); + if ($this->checkTablesExist()) { + throw new Exception("ERROR: airtime tables already exists"); } + //$this->createDatabase(); //All we need to do is create the database tables. + $this->createDatabaseTables(); $this->initializeMusicDirsTable($this->instanceId); } catch (Exception $e) { http_response_code(400); - Logging::error($e->getMessage()); - echo $e->getMessage(); + Logging::error($e->getMessage() + ); + echo $e->getMessage() . PHP_EOL; return; } @@ -70,6 +81,19 @@ class ProvisioningHelper 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() { $this->dbuser = $_POST['dbuser']; @@ -87,9 +111,11 @@ class ProvisioningHelper private function setNewDatabaseConnection() { self::$dbh = new PDO("pgsql:host=" . $this->dbhost - . ";dbname=postgres" + . ";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(); if ($err[1] != null) { throw new PDOException("ERROR: Could not connect to database"); From 78c7170c4e629a887799b116fdf9cbe41abc3ea3 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Fri, 27 Feb 2015 17:19:37 -0500 Subject: [PATCH 27/46] Reformatted logo remove button to use Zend --- airtime_mvc/application/forms/GeneralPreferences.php | 7 +++++++ .../views/scripts/form/preferences_general.phtml | 6 ++++-- airtime_mvc/public/css/styles.css | 7 ++++++- airtime_mvc/public/js/airtime/preferences/preferences.js | 9 +++++---- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php index 52f14332a..aa2906d82 100644 --- a/airtime_mvc/application/forms/GeneralPreferences.php +++ b/airtime_mvc/application/forms/GeneralPreferences.php @@ -49,6 +49,13 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm $stationLogoUpload->setAttrib('accept', 'image/*'); $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 $this->addElement('text', 'stationDefaultCrossfadeDuration', array( 'class' => 'input_text', diff --git a/airtime_mvc/application/views/scripts/form/preferences_general.phtml b/airtime_mvc/application/views/scripts/form/preferences_general.phtml index 99cb4384c..85e9a9b7e 100644 --- a/airtime_mvc/application/views/scripts/form/preferences_general.phtml +++ b/airtime_mvc/application/views/scripts/form/preferences_general.phtml @@ -7,10 +7,12 @@ element->getElement('stationLogo')->render() ?> - + element->getElement('stationLogoRemove')->render() ?> + +
- +
element->getElement('locale')->render() ?> diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index 05987d876..44e0db75b 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -2187,12 +2187,17 @@ dd.radio-inline-list, .preferences dd.radio-inline-list, .stream-config dd.radio height: 120px; } +.preferences #stationLogoRemove-label { + display: none; +} + .preferences #logo-remove-btn { float: right; + margin-bottom: 4px; } .preferences #Logo-img-container { - float: left; + margin-top: 30px; } #show_time_info { diff --git a/airtime_mvc/public/js/airtime/preferences/preferences.js b/airtime_mvc/public/js/airtime/preferences/preferences.js index f7875ec7b..faef03218 100644 --- a/airtime_mvc/public/js/airtime/preferences/preferences.js +++ b/airtime_mvc/public/js/airtime/preferences/preferences.js @@ -96,6 +96,11 @@ function setSoundCloudCheckBoxListener() { }); } +function removeLogo() { + $.post(baseUrl+'Preference/remove-logo', function(json){}); + location.reload(); +} + $(document).ready(function() { $('.collapsible-header').live('click',function() { @@ -104,10 +109,6 @@ $(document).ready(function() { return false; }).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 $('#pref_save').live('click', function() { var data = $('#pref_form').serialize(); From 13bd0b758934d4fe229fde33560b459d5cdcd424 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Mon, 2 Mar 2015 14:57:50 -0500 Subject: [PATCH 28/46] Fixed removing image when saving preferences with no upload --- .../application/controllers/PreferenceController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 09188b93d..7d23a958e 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -51,7 +51,11 @@ class PreferenceController extends Zend_Controller_Action $logoUploadElement = $form->getSubForm('preferences_general')->getElement('stationLogo'); $logoUploadElement->receive(); $imagePath = $logoUploadElement->getFileName(); - Application_Model_Preference::SetStationLogo($imagePath); + + // Only update the image logo if the new logo is non-empty + if (!is_null($imagePath) && $imagePath != "") { + Application_Model_Preference::SetStationLogo($imagePath); + } Application_Model_Preference::SetEnableSystemEmail($values["enableSystemEmail"]); Application_Model_Preference::SetSystemEmail($values["systemEmail"]); From b6a6f038a96711ef09799b31284177c268566e9a Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Mon, 2 Mar 2015 15:10:04 -0500 Subject: [PATCH 29/46] Added call to setStationDescription in preferences action --- airtime_mvc/application/controllers/PreferenceController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 7d23a958e..0e13eb3ec 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -40,6 +40,7 @@ class PreferenceController extends Zend_Controller_Action if ($form->isValid($values)) { Application_Model_Preference::SetHeadTitle($values["stationName"], $this->view); + Application_Model_Preference::SetStationDescription($values["stationDescription"]); Application_Model_Preference::SetDefaultCrossfadeDuration($values["stationDefaultCrossfadeDuration"]); Application_Model_Preference::SetDefaultFadeIn($values["stationDefaultFadeIn"]); Application_Model_Preference::SetDefaultFadeOut($values["stationDefaultFadeOut"]); From 0272eaef444892263afd6b626c1d13cc122f7a6e Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Mon, 2 Mar 2015 15:25:52 -0500 Subject: [PATCH 30/46] Changed is_null to empty --- airtime_mvc/application/controllers/PreferenceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 0e13eb3ec..25aae1d10 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -54,7 +54,7 @@ class PreferenceController extends Zend_Controller_Action $imagePath = $logoUploadElement->getFileName(); // Only update the image logo if the new logo is non-empty - if (!is_null($imagePath) && $imagePath != "") { + if (!empty($imagePath) && $imagePath != "") { Application_Model_Preference::SetStationLogo($imagePath); } From 17d51eb0f9e133e8dd64bd4a03eb8f8fd1c1e44a Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Mon, 2 Mar 2015 16:00:11 -0500 Subject: [PATCH 31/46] Added csrf verification to show image upload and deletion --- airtime_mvc/application/forms/AddShowStyle.php | 10 +++++++++- airtime_mvc/public/js/airtime/schedule/add-show.js | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/airtime_mvc/application/forms/AddShowStyle.php b/airtime_mvc/application/forms/AddShowStyle.php index d6e97e019..7d79a90e9 100644 --- a/airtime_mvc/application/forms/AddShowStyle.php +++ b/airtime_mvc/application/forms/AddShowStyle.php @@ -79,7 +79,7 @@ class Application_Form_AddShowStyle extends Zend_Form_SubForm ->addValidator('Count', false, 1) ->addValidator('Extension', false, 'jpg,jpeg,png,gif') ->addFilter('ImageSize'); - + $this->addElement($upload); // Add image preview @@ -93,6 +93,14 @@ class Application_Form_AddShowStyle extends Zend_Form_SubForm 'class' => 'big' )))); $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() diff --git a/airtime_mvc/public/js/airtime/schedule/add-show.js b/airtime_mvc/public/js/airtime/schedule/add-show.js index aee048fd8..a8c9f76e7 100644 --- a/airtime_mvc/public/js/airtime/schedule/add-show.js +++ b/airtime_mvc/public/js/airtime/schedule/add-show.js @@ -668,7 +668,7 @@ function setAddShowEvents(form) { var showId = $("#add_show_id").attr("value"); 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({ url: action, @@ -748,7 +748,7 @@ function setAddShowEvents(form) { data: {format: "json", data: data, hosts: hosts, days: days}, success: function(json) { 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 $.ajax({ From 8a6ee8ad14da44e171a0fea33fcda2d48b4c23c2 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Tue, 3 Mar 2015 12:52:41 -0500 Subject: [PATCH 32/46] Fixed /rest/media endpoint DELETE --- airtime_mvc/application/models/airtime/CcFiles.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index c1c444674..337e3b80b 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -280,10 +280,8 @@ class CcFiles extends BaseCcFiles { $file = CcFilesQuery::create()->findPk($id); if ($file) { $con = Propel::getConnection(); - $storedFile = new Application_Model_StoredFile($file, $con); - if ($storedFile->existsOnDisk()) { - $storedFile->delete(); //TODO: This checks your session permissions... Make it work without a session? - } + $storedFile = Application_Model_StoredFile::RecallById($id, $con); + $storedFile->delete(); } else { throw new FileNotFoundException(); } From 1f6f880dad6d54cd238d4df404c3e721855e9887 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Tue, 3 Mar 2015 13:44:42 -0500 Subject: [PATCH 33/46] Fixed deleting failed uploads --- airtime_mvc/application/models/StoredFile.php | 4 +++- airtime_mvc/application/models/airtime/CloudFile.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 0e15dcb92..5e6e53161 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -398,7 +398,9 @@ SQL; //Delete the physical file from either the local stor directory //or from the cloud - $this->_file->deletePhysicalFile(); + if ($this->_file->getDbImportStatus() == CcFiles::IMPORT_STATUS_SUCCESS) { + $this->_file->deletePhysicalFile(); + } //Update the user's disk usage Application_Model_Preference::updateDiskUsage(-1 * $filesize); diff --git a/airtime_mvc/application/models/airtime/CloudFile.php b/airtime_mvc/application/models/airtime/CloudFile.php index fdc609ffc..50b805ab1 100644 --- a/airtime_mvc/application/models/airtime/CloudFile.php +++ b/airtime_mvc/application/models/airtime/CloudFile.php @@ -5,6 +5,9 @@ require_once 'ProxyStorageBackend.php'; /** * 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. * The file's metadata is stored in the cc_file table. This, cloud_file, * table represents files that are stored in the cloud. From 278aa31d2f91109ffa4c5c709c4e863033a2166e Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 4 Mar 2015 15:53:43 -0500 Subject: [PATCH 34/46] Hack: avoid calling WHMCS to get the station instance id --- airtime_mvc/application/common/Billing.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/airtime_mvc/application/common/Billing.php b/airtime_mvc/application/common/Billing.php index a4bb72150..6753f2326 100644 --- a/airtime_mvc/application/common/Billing.php +++ b/airtime_mvc/application/common/Billing.php @@ -14,8 +14,13 @@ class Billing /** Get the Airtime instance ID of the instance the customer is currently viewing. */ public static function getClientInstanceId() { - $currentProduct = Billing::getClientCurrentAirtimeProduct(); - return $currentProduct["id"]; + #$currentProduct = Billing::getClientCurrentAirtimeProduct(); + #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']['username']; + return $instanceId; } public static function getProducts() @@ -318,4 +323,4 @@ class Billing $result = Billing::makeRequest($credentials["url"], $query_string); } -} \ No newline at end of file +} From df28b47b925032b09227b546c77789ea0d22219b Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 4 Mar 2015 16:26:35 -0500 Subject: [PATCH 35/46] SAAS-624: Request to rest/media throws an exception if the data set is too large --- .../rest/controllers/MediaController.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index acb0eeed7..c2f140666 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -14,15 +14,27 @@ class Rest_MediaController extends Zend_Rest_Controller public function indexAction() { - $files_array = array(); - foreach (CcFilesQuery::create()->find() as $file) - { - array_push($files_array, CcFiles::sanitizeResponse($file)); + $pager = CcFilesQuery::create()->paginate($page=1, $maxPerPage=50); + $numPages = $pager->getLastPage(); + + $nextPage = 1; + + while ($nextPage <= $numPages) { + $pager = CcFilesQuery::create()->paginate($page=$nextPage, $maxPerPage=50); + + $files = array(); + foreach($pager->getResults() as $file) { + array_push($files, CcFiles::sanitizeResponse($file)); + } + $this->getResponse()->appendBody(json_encode($files)); + unset($files); + + $nextPage +=1; + } $this->getResponse() - ->setHttpResponseCode(200) - ->appendBody(json_encode($files_array)); + ->setHttpResponseCode(200); /** TODO: Use this simpler code instead after we upgrade to Propel 1.7 (Airtime 2.6.x branch): $this->getResponse() From 181db7617aa168eaabf0745db3f380bc3c801e61 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 4 Mar 2015 17:02:30 -0500 Subject: [PATCH 36/46] Use Billing::getClientInstanceId() in Amazon_S3StorageBackend --- .../application/cloud_storage/Amazon_S3StorageBackend.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php index 406e81016..e2b6a1ba6 100644 --- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php +++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php @@ -60,8 +60,7 @@ class Amazon_S3StorageBackend extends StorageBackend public function getFilePrefix() { - $clientCurrentAirtimeProduct = Billing::getClientCurrentAirtimeProduct(); - $hostingId = $clientCurrentAirtimeProduct["id"]; + $hostingId = Billing::getClientInstanceId(); return substr($hostingId, -2)."/".$hostingId; } } From 62d6849d4e71062f6149aeb69de4e34f3a4d0928 Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 5 Mar 2015 14:25:48 -0500 Subject: [PATCH 37/46] SAAS-624: Request to rest/media throws an exception if the data set is too large Added offset and limit parameters to /rest/media index action. --- .../rest/controllers/MediaController.php | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index c2f140666..6a116c13f 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -14,27 +14,31 @@ class Rest_MediaController extends Zend_Rest_Controller public function indexAction() { - $pager = CcFilesQuery::create()->paginate($page=1, $maxPerPage=50); - $numPages = $pager->getLastPage(); + $totalFileCount = BaseCcFilesQuery::create()->count(); - $nextPage = 1; + // 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); - while ($nextPage <= $numPages) { - $pager = CcFilesQuery::create()->paginate($page=$nextPage, $maxPerPage=50); - - $files = array(); - foreach($pager->getResults() as $file) { - array_push($files, CcFiles::sanitizeResponse($file)); - } - $this->getResponse()->appendBody(json_encode($files)); - unset($files); - - $nextPage +=1; + $query = CcFilesQuery::create() + ->filterByDbHidden(false) + ->filterByDbImportStatus(0) + ->setLimit($limit) + ->setOffset($offset); + $queryCount = $query->count(); + $queryResult = $query->find(); + $files_array = array(); + foreach ($queryResult as $file) + { + array_push($files_array, CcFiles::sanitizeResponse($file)); } - + $this->getResponse() - ->setHttpResponseCode(200); + ->setHttpResponseCode(200) + ->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): $this->getResponse() From df48de607b552971514bdf5a1a516a9d39e47393 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 5 Mar 2015 17:45:07 -0500 Subject: [PATCH 38/46] Revert billing client ID change --- airtime_mvc/application/common/Billing.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/airtime_mvc/application/common/Billing.php b/airtime_mvc/application/common/Billing.php index 6753f2326..b1a2f8a1f 100644 --- a/airtime_mvc/application/common/Billing.php +++ b/airtime_mvc/application/common/Billing.php @@ -14,12 +14,12 @@ class Billing /** Get the Airtime instance ID of the instance the customer is currently viewing. */ public static function getClientInstanceId() { - #$currentProduct = Billing::getClientCurrentAirtimeProduct(); - #return $currentProduct["id"]; + $currentProduct = Billing::getClientCurrentAirtimeProduct(); + 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']['username']; + //$CC_CONFIG = Config::getConfig(); + //$instanceId = $CC_CONFIG['rabbitmq']['username']; return $instanceId; } From 8cdecff02285888b572df40723c3aeb50e16ee01 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Thu, 5 Mar 2015 18:22:04 -0500 Subject: [PATCH 39/46] Fixed Billing::GetClientInstanceId() --- airtime_mvc/application/common/Billing.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/common/Billing.php b/airtime_mvc/application/common/Billing.php index 6753f2326..8927cbc73 100644 --- a/airtime_mvc/application/common/Billing.php +++ b/airtime_mvc/application/common/Billing.php @@ -19,7 +19,10 @@ class Billing //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']['username']; + $instanceId = $CC_CONFIG['rabbitmq']['user']; + if (!is_numeric($instanceId)) { + throw new Exception("Invalid instance id in " . __FUNCTION__ . ": " . $instanceId); + } return $instanceId; } From 962f5d925d187a3bfaae4e96f7a62733559627d0 Mon Sep 17 00:00:00 2001 From: drigato Date: Fri, 6 Mar 2015 09:32:14 -0500 Subject: [PATCH 40/46] SAAS-624: Request to rest/media throws an exception if the data set is too large Fixed small typo --- .../application/modules/rest/controllers/MediaController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 6a116c13f..5f41162ef 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -14,7 +14,7 @@ class Rest_MediaController extends Zend_Rest_Controller public function indexAction() { - $totalFileCount = BaseCcFilesQuery::create()->count(); + $totalFileCount = CcFilesQuery::create()->count(); // Check if offset and limit were sent with request. // Default limit to zero and offset to $totalFileCount From 6cecb7c23955ed87dda376469a59f50ba3bea938 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Fri, 6 Mar 2015 11:06:17 -0500 Subject: [PATCH 41/46] Simplified the FTP upload hook --- .../application/models/airtime/CcFiles.php | 16 +++------------- .../airtime_analyzer/tools/ftp-upload-hook.sh | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 337e3b80b..71e5f2ca6 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -90,19 +90,9 @@ class CcFiles extends BaseCcFiles { * files won't get removed from the organize folder. */ - //Extract the relative path to the temporary uploaded file on disk. - if (isset($fileArray["full_path"])) { - $fullPath = $fileArray["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 - $filePathRelativeToOrganize = substr($fullPath, strlen($basePath)-1); - $originalFilename = $filePathRelativeToOrganize; - } else { - //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"]; - } - + //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 { diff --git a/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh b/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh index f0a00fbe9..aa543f853 100755 --- a/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh +++ b/python_apps/airtime_analyzer/tools/ftp-upload-hook.sh @@ -35,7 +35,7 @@ post_file() { # -f is needed to make curl fail if there's an HTTP error code # -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 retry_count=$[$retry_count+1] if [ $retry_count -ge $max_retry ]; then From 47e6879766f1c20dd648e656c7fc58b822c4fba7 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Fri, 6 Mar 2015 11:18:48 -0500 Subject: [PATCH 42/46] Fixed Billing.php again --- airtime_mvc/application/common/Billing.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/common/Billing.php b/airtime_mvc/application/common/Billing.php index 2cfdbdb24..fa716f6d1 100644 --- a/airtime_mvc/application/common/Billing.php +++ b/airtime_mvc/application/common/Billing.php @@ -14,8 +14,8 @@ class Billing /** Get the Airtime instance ID of the instance the customer is currently viewing. */ public static function getClientInstanceId() { - $currentProduct = Billing::getClientCurrentAirtimeProduct(); - return $currentProduct["id"]; + //$currentProduct = Billing::getClientCurrentAirtimeProduct(); + //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(); From e315498a5cdeebb58aa0e3c929dd469f8edb071f Mon Sep 17 00:00:00 2001 From: drigato Date: Fri, 6 Mar 2015 12:21:51 -0500 Subject: [PATCH 43/46] SAAS-624: Request to rest/media throws an exception if the data set is too large Modified /rest/media to return items where file_exists = true --- .../application/modules/rest/controllers/MediaController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 5f41162ef..44d037e83 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -23,6 +23,7 @@ class Rest_MediaController extends Zend_Rest_Controller $query = CcFilesQuery::create() ->filterByDbHidden(false) + ->filterByDbFileExists(true) ->filterByDbImportStatus(0) ->setLimit($limit) ->setOffset($offset); From b398fea7e771fd1a6d63eb8e4b2caefa3d2387bc Mon Sep 17 00:00:00 2001 From: drigato Date: Tue, 10 Mar 2015 12:29:30 -0400 Subject: [PATCH 44/46] SAAS-628: Create script to update disk_usage for all Airtime pro stations --- .../controllers/UpgradeController.php | 1 + airtime_mvc/application/upgrade/Upgrades.php | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php index c9748467b..9af3dc1e9 100644 --- a/airtime_mvc/application/controllers/UpgradeController.php +++ b/airtime_mvc/application/controllers/UpgradeController.php @@ -19,6 +19,7 @@ class UpgradeController extends Zend_Controller_Action array_push($upgraders, new AirtimeUpgrader255()); array_push($upgraders, new AirtimeUpgrader259()); array_push($upgraders, new AirtimeUpgrader2510()); + array_push($upgraders, new AirtimeUpgrader2511()); $didWePerformAnUpgrade = false; try diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php index 74b9a6d4d..31c602437 100644 --- a/airtime_mvc/application/upgrade/Upgrades.php +++ b/airtime_mvc/application/upgrade/Upgrades.php @@ -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; + } + } +} From 5b3edaa2ca61b141a497dd0e21850060de6be8e0 Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 11 Mar 2015 13:25:27 -0400 Subject: [PATCH 45/46] Add order by clause to /rest/media index action --- .../application/modules/rest/controllers/MediaController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 44d037e83..6a2cccd63 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -26,7 +26,8 @@ class Rest_MediaController extends Zend_Rest_Controller ->filterByDbFileExists(true) ->filterByDbImportStatus(0) ->setLimit($limit) - ->setOffset($offset); + ->setOffset($offset) + ->orderByDbId(); $queryCount = $query->count(); $queryResult = $query->find(); From 55ed21a15517ba44d7f740633748118722448f5c Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 11 Mar 2015 13:50:17 -0400 Subject: [PATCH 46/46] SAAS-606: Enable disk quota on Airtime Pro --- airtime_mvc/application/controllers/PluploadController.php | 5 ++--- airtime_mvc/application/models/airtime/CcFiles.php | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/airtime_mvc/application/controllers/PluploadController.php b/airtime_mvc/application/controllers/PluploadController.php index c3fa239e9..7c808140c 100644 --- a/airtime_mvc/application/controllers/PluploadController.php +++ b/airtime_mvc/application/controllers/PluploadController.php @@ -27,10 +27,9 @@ class PluploadController extends Zend_Controller_Action $this->view->headLink()->appendStylesheet($baseUrl.'css/addmedia.css?'.$CC_CONFIG['airtime_version']); $this->view->quotaLimitReached = false; - // temporarily disabling disk quota until all file size values have been set - /*if (Application_Model_Systemstatus::isDiskOverQuota()) { + if (Application_Model_Systemstatus::isDiskOverQuota()) { $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. diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php index 71e5f2ca6..029d21a15 100644 --- a/airtime_mvc/application/models/airtime/CcFiles.php +++ b/airtime_mvc/application/models/airtime/CcFiles.php @@ -78,11 +78,9 @@ class CcFiles extends BaseCcFiles { */ public static function createFromUpload($fileArray) { - /*temporarily disabling disk quota until all file sizes have ben set in the database. 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