From 19a7e7eb5d6db7da6a14ace9006da78ad6ec8668 Mon Sep 17 00:00:00 2001 From: Naomi Date: Fri, 1 Nov 2013 18:02:10 -0400 Subject: [PATCH] CC-5450 : Refactor Media Management refactored webstreams, new/save is working adding validators/filters for the url separating out parsing of different webstream formats. --- airtime_mvc/application/Bootstrap.php | 10 +++ .../controllers/WebstreamController.php | 45 +++++++++- airtime_mvc/application/forms/Webstream.php | 62 +++++++++++++ .../forms/filters/WebstreamRedirect.php | 51 +++++++++++ .../forms/validators/WebstreamUrl.php | 87 +++++++++++++++++++ .../application/models/airtime/Webstream.php | 36 ++++++++ .../models/webstream/M3UWebstreamParser.php | 32 +++++++ .../models/webstream/PLSWebstreamParser.php | 25 ++++++ .../models/webstream/WebstreamParser.php | 10 +++ .../models/webstream/XSPFWebstreamParser.php | 33 +++++++ .../application/services/WebstreamService.php | 87 +++++++++++++++++++ .../views/scripts/form/webstream.phtml | 69 +++++++++++++++ airtime_mvc/public/js/airtime/library/spl.js | 44 ++++++---- 13 files changed, 569 insertions(+), 22 deletions(-) create mode 100644 airtime_mvc/application/forms/Webstream.php create mode 100644 airtime_mvc/application/forms/filters/WebstreamRedirect.php create mode 100644 airtime_mvc/application/forms/validators/WebstreamUrl.php create mode 100644 airtime_mvc/application/models/webstream/M3UWebstreamParser.php create mode 100644 airtime_mvc/application/models/webstream/PLSWebstreamParser.php create mode 100644 airtime_mvc/application/models/webstream/WebstreamParser.php create mode 100644 airtime_mvc/application/models/webstream/XSPFWebstreamParser.php create mode 100644 airtime_mvc/application/services/WebstreamService.php create mode 100644 airtime_mvc/application/views/scripts/form/webstream.phtml diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 313e448c8..439b11f60 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -186,5 +186,15 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap 'action' => 'password-change', ))); } + + protected function _initAutoload () { + + // configure new autoloader + $autoloader = new Zend_Application_Module_Autoloader (array ('namespace' => '', 'basePath' => APPLICATION_PATH)); + + // autoload form validators & filters definition + $autoloader->addResourceType ('Filter', 'forms/filters', 'Filter_'); + $autoloader->addResourceType ('Validator', 'forms/validators', 'Validate_'); + } } diff --git a/airtime_mvc/application/controllers/WebstreamController.php b/airtime_mvc/application/controllers/WebstreamController.php index dad383fe6..4ffb99e68 100644 --- a/airtime_mvc/application/controllers/WebstreamController.php +++ b/airtime_mvc/application/controllers/WebstreamController.php @@ -17,7 +17,22 @@ class WebstreamController extends Zend_Controller_Action public function newAction() { + //clear the session in case an old playlist was open: CC-4196 + Application_Model_Library::changePlaylist(null, null); + + $service = new Application_Service_WebstreamService(); + $form = $service->makeWebstreamForm(null); + + $form->setDefaults(array( + 'name' => 'Unititled Webstream', + 'hours' => 0, + 'mins' => 30, + )); + + $this->view->action = "new"; + $this->view->html = $form->render(); + /* $userInfo = Zend_Auth::getInstance()->getStorage()->read(); if (!$this->isAuthorized(-1)) { // TODO: this header call does not actually print any error message @@ -44,6 +59,7 @@ class WebstreamController extends Zend_Controller_Action $this->view->obj = new Application_Model_Webstream($webstream); $this->view->action = "new"; $this->view->html = $this->view->render('webstream/webstream.phtml'); + */ } public function editAction() @@ -117,20 +133,40 @@ class WebstreamController extends Zend_Controller_Action public function saveAction() { $request = $this->getRequest(); - - $id = $request->getParam("id"); - $parameters = array(); - foreach (array('id','length','name','description','url') as $p) { + + foreach (array('id','hours', 'mins', 'name','description','url') as $p) { $parameters[$p] = trim($request->getParam($p)); } + + Logging::info($parameters); + + $service = new Application_Service_WebstreamService(); + $form = $service->makeWebstreamForm(null); + + if ($form->isValid($parameters)) { + Logging::info("form is valid"); + + $values = $form->getValues(); + Logging::info($values); + + $service->saveWebstream($values); + } + else { + Logging::info("form is not valid"); + } + + + /* if (!$this->isAuthorized($id)) { header("Status: 401 Not Authorized"); return; } + */ + /* list($analysis, $mime, $mediaUrl, $di) = Application_Model_Webstream::analyzeFormData($parameters); try { if (Application_Model_Webstream::isValid($analysis)) { @@ -150,5 +186,6 @@ class WebstreamController extends Zend_Controller_Action $this->view->streamId = -1; $this->view->analysis = $analysis; } + */ } } diff --git a/airtime_mvc/application/forms/Webstream.php b/airtime_mvc/application/forms/Webstream.php new file mode 100644 index 000000000..8431868fc --- /dev/null +++ b/airtime_mvc/application/forms/Webstream.php @@ -0,0 +1,62 @@ +setDecorators( + array( + array('ViewScript', array('viewScript' => 'form/webstream.phtml')) + ) + ); + + $id = new Zend_Form_Element_Hidden('id'); + $id->setValidators(array( + new Zend_Validate_Int() + )); + $this->addElement($id); + + $name = new Zend_Form_Element_Text('name'); + $name->setLabel(_('Firstname:')); + $name->setAttrib('class', 'input_text'); + $name->addFilter('StringTrim'); + $this->addElement($name); + + $description = new Zend_Form_Element_Text('description'); + $description->setLabel(_('Description:')); + $description->setAttrib('class', 'input_text_area'); + $description->addFilter('StringTrim'); + $description->setValidators(array( + new Zend_Validate_StringLength(array('max' => 512)), + )); + $this->addElement($description); + + $url = new Zend_Form_Element_Text('url'); + $url->setLabel(_('Stream URL:')); + $url->setAttrib('class', 'input_text'); + $url->addFilter('StringTrim'); + $url->addFilter(new Filter_WebstreamRedirect()); + $url->setValidators(array( + new Validate_WebstreamUrl(), + )); + $this->addElement($url); + + $hours = new Zend_Form_Element_Text('hours'); + $hours->setLabel(_('h')); + $hours->setAttrib('class', 'input_text'); + $hours->addFilter('StringTrim'); + $hours->setValidators(array( + new Zend_Validate_Int(), + )); + $this->addElement($hours); + + $min = new Zend_Form_Element_Text('mins'); + $min->setLabel(_('m')); + $min->setAttrib('class', 'input_text'); + $min->addFilter('StringTrim'); + $min->setValidators(array( + new Zend_Validate_Int(), + )); + $this->addElement($min); + } +} \ No newline at end of file diff --git a/airtime_mvc/application/forms/filters/WebstreamRedirect.php b/airtime_mvc/application/forms/filters/WebstreamRedirect.php new file mode 100644 index 000000000..e8cd17f7b --- /dev/null +++ b/airtime_mvc/application/forms/filters/WebstreamRedirect.php @@ -0,0 +1,51 @@ + array( + 'method' => 'HEAD', + 'max_redirects' => '1' + ) + ) + ); + + try { + + $url = $value; + + while (true) { + //get an associative array of headers. + $headers = get_headers($url, 1); + Logging::info($headers); + + if (empty($headers["Location"])) { + //is not a redirect; + break; + } + else { + $url = $headers["Location"]; + } + } + + return $url; + } + catch (Exception $e) { + throw new Zend_Filter_Exception( _("Invalid webstream url")); + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/forms/validators/WebstreamUrl.php b/airtime_mvc/application/forms/validators/WebstreamUrl.php new file mode 100644 index 000000000..e0330521c --- /dev/null +++ b/airtime_mvc/application/forms/validators/WebstreamUrl.php @@ -0,0 +1,87 @@ + false, + "/xspf\+xml/" => false, + "/pls\+xml|x-scpls/" => false, + "/(mpeg|ogg|audio\/aacp)/" => true + ); + + public function __construct() + { + $this->_messageTemplates = array( + self::INVALID_WEBSTREAM_URL => _("Invalid webstream url"), + self::INVALID_MIME_TYPE => _("Invalid webstream mime type"), + ); + } + + private function validateNoContentLength($headers) + { + return isset($headers["Content-Length"]) ? false : true; + } + + public function isValid($value) + { + Logging::info("checking if $value is valid"); + + // By default get_headers uses a GET request to fetch the headers. If you + // want to send a HEAD request instead, you can do so using a stream context: + //using max redirects to avoid mixed headers, + //can manually follow redirects if a Location header exists. + stream_context_set_default( + array( + 'http' => array( + 'method' => 'HEAD' + ) + ) + ); + + try { + //get an associative array of headers. + $headers = get_headers($value, 1); + + if (empty($headers["Content-Type"])) { + $this->_error(self::INVALID_MIME_TYPE); + return false; + } + + $contentType = $headers["Content-Type"]; + Logging::info($contentType); + + $isValid = false; + + foreach ($this->_validMimeTypePatterns as $pattern => $doContentLengthCheck) { + + if (preg_match($pattern, $contentType)) { + + if ($doContentLengthCheck) { + $isValid = self::validateNoContentLength($headers); + } + else { + $isValid = true; + } + + break; + } + } + + if ($isValid === false) { + $this->_error(self::INVALID_MIME_TYPE); + } + + return $isValid; + } + catch (Exception $e) { + //url is not real + $this->_error(self::INVALID_WEBSTREAM_URL); + return false; + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/models/airtime/Webstream.php b/airtime_mvc/application/models/airtime/Webstream.php index 1f6d32c7f..ae0449150 100644 --- a/airtime_mvc/application/models/airtime/Webstream.php +++ b/airtime_mvc/application/models/airtime/Webstream.php @@ -18,4 +18,40 @@ use Airtime\MediaItem\om\BaseWebstream; */ class Webstream extends BaseWebstream { + public function getHoursMins() { + + return explode(":", $this->getLength()); + } + + public function getUrlData() { + + $url = $this->getUrl(); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + // grab URL and pass it to the browser + //TODO: What if invalid url? + $content = curl_exec($ch); + + // close cURL resource, and free up system resources + curl_close($ch); + + return $content; + } + + public function setUrl($v) { + + parent::setUrl($v); + + //get an associative array of headers. + $headers = get_headers($v, 1); + $mime = $headers["Content-Type"]; + + $this->setMime($mime); + + return $this; + } } diff --git a/airtime_mvc/application/models/webstream/M3UWebstreamParser.php b/airtime_mvc/application/models/webstream/M3UWebstreamParser.php new file mode 100644 index 000000000..caacc9c63 --- /dev/null +++ b/airtime_mvc/application/models/webstream/M3UWebstreamParser.php @@ -0,0 +1,32 @@ +getUrlData(); + + //split into lines: + $delim = "\n"; + if (strpos($content, "\r\n") !== false) { + $delim = "\r\n"; + } + + $lines = explode($delim, $content); + + //TODO test the stream + return $lines[0]; + + } + catch (Exception $e) { + Logging::warn($e->getMessage()); + throw new Exception(_("Could not parse M3U playlist")); + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/models/webstream/PLSWebstreamParser.php b/airtime_mvc/application/models/webstream/PLSWebstreamParser.php new file mode 100644 index 000000000..1c75fb7e0 --- /dev/null +++ b/airtime_mvc/application/models/webstream/PLSWebstreamParser.php @@ -0,0 +1,25 @@ +getUrlData(); + $ini = parse_ini_string($content, true); + + //TODO test the stream + return $ini["playlist"]["File1"]; + } + catch (Exception $e) { + Logging::warn($e->getMessage()); + throw new Exception(_("Could not parse PLS playlist")); + } + } +} diff --git a/airtime_mvc/application/models/webstream/WebstreamParser.php b/airtime_mvc/application/models/webstream/WebstreamParser.php new file mode 100644 index 000000000..c220a0ec1 --- /dev/null +++ b/airtime_mvc/application/models/webstream/WebstreamParser.php @@ -0,0 +1,10 @@ +getUrlData(); + + $dom = new DOMDocument; + + $dom->loadXML($content); + $tracks = $dom->getElementsByTagName('track'); + + foreach ($tracks as $track) { + $locations = $track->getElementsByTagName('location'); + foreach ($locations as $loc) { + return $loc->nodeValue; + } + } + } + catch (Exception $e) { + Logging::warn($e->getMessage()); + throw new Exception(_("Could not parse XSPF playlist")); + } + } +} diff --git a/airtime_mvc/application/services/WebstreamService.php b/airtime_mvc/application/services/WebstreamService.php new file mode 100644 index 000000000..726e352a4 --- /dev/null +++ b/airtime_mvc/application/services/WebstreamService.php @@ -0,0 +1,87 @@ +findPk($id); + $length = $webstream->getHoursMins(); + + $formValues = array( + "name" => $webstream->getName(), + "description" => $webstream->getDescription(), + "url" => $webstream->getUrl(), + "hours" => $length[0], + "mins" => $length[1] + ); + + $form->populate($formValues); + } + + return $form; + } + catch (Exception $e) { + Logging::info($e); + throw $e; + } + } + + private function buildFromFormValues($ws, $values) { + + $hours = intval($values["hours"]); + $minutes = intval($values["mins"]); + + if ($minutes > 59) { + //minutes cannot be over 59. Need to convert anything > 59 minutes into hours. + $hours += intval($minutes/60); + $minutes = $minutes%60; + } + + $length = "$hours:$minutes"; + + $ws->setName($values["name"]); + $ws->setDescription($values["description"]); + $ws->setUrl($values["url"]); + $ws->setLength($length); + + return $ws; + } + + public function createWebstream($values) { + + $ws = new Webstream(); + $ws = self::buildFromFormValues($ws, $values); + + return $ws; + } + + public function updateWebstream($values) { + + $ws = WebstreamQuery::create()->findPk($values["id"]); + $ws = self::buildFromFormValues($ws, $values); + + return $ws; + } + + public function saveWebstream($values) { + + if ($values["id"] != null) { + $ws = self::updateWebstream($values); + } + else { + $ws = self::createWebstream($values); + } + $ws->save(); + + return $ws; + } +} \ No newline at end of file diff --git a/airtime_mvc/application/views/scripts/form/webstream.phtml b/airtime_mvc/application/views/scripts/form/webstream.phtml new file mode 100644 index 000000000..114f2f0a5 --- /dev/null +++ b/airtime_mvc/application/views/scripts/form/webstream.phtml @@ -0,0 +1,69 @@ +element ?> + + +
+
+ + +
+ + +
+ +
+
+ +
+ +
+ + + getValue() ?>"> + obj->getLastModified('U'); ?>"> + + +
+

+ getElement("name")->getValue() ?> +

+

00h 00m

+
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ getValue() ?>" size="60"/> +
+ +
+ +
+
+ getValue() ?>" size="2"/> + getValue() ?>" size="2"/> +
+
+ + +
+ \ No newline at end of file diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index b33a16942..f2f9ddbc5 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -780,27 +780,37 @@ var AIRTIME = (function(AIRTIME){ disableLoadingIcon(); setTimeout(removeSuccessMsg, 5000); }); - }) + }); - $pl.on("click", "#webstream_save", function(){ + $pl.on("click", "#webstream_save", function() { //get all fields and POST to server - //description - //stream url - //default_length - //playlist name - var id = $pl.find("#obj_id").attr("value"); - var description = $pl.find("#description").val(); - var streamurl = $pl.find("#streamurl-element input").val(); - var length = $pl.find("#streamlength-element input").val(); - var name = $pl.find("#playlist_name_display").text(); + var id = $pl.find("#ws_id").attr("value"); + var description = $pl.find("#ws_description").val(); + var streamurl = $pl.find("#ws_url").val(); + var hours = $pl.find("#ws_hours").val(); + var mins = $pl.find("#ws_mins").val(); + var name = $pl.find("#ws_name").text(); + + var parameters = { + format: "json", + description: description, + url: streamurl, + hours: hours, + mins: mins, + name: name + }; + + if (id !== "") { + parameters["id"] = id; + } //hide any previous errors (if any) $("#side_playlist .errors").empty().hide(); var url = baseUrl+'Webstream/save'; $.post(url, - {format: "json", id:id, description: description, url:streamurl, length: length, name: name}, - function(json){ + parameters, + function(json) { if (json.analysis){ for (var s in json.analysis){ var field = json.analysis[s]; @@ -810,7 +820,8 @@ var AIRTIME = (function(AIRTIME){ var $div = $("#side_playlist " + elemId).text(field[1]).show(); } } - } else { + } + else { var $status = $("#side_playlist .status"); $status.html(json.statusMessage); $status.show(); @@ -828,11 +839,8 @@ var AIRTIME = (function(AIRTIME){ //redraw the library to show the new webstream redrawLib(); - } - + } }); - - }); $lib.on("click", "#pl_edit", function() {