Webstream.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <?php
  2. class Application_Model_Webstream implements Application_Model_LibraryEditable
  3. {
  4. private $id;
  5. public function __construct($webstream)
  6. {
  7. //TODO: hacky...
  8. if (is_int($webstream)) {
  9. $this->webstream = CcWebstreamQuery::create()->findPK($webstream);
  10. if (is_null($this->webstream)) {
  11. throw new Exception();
  12. }
  13. } else {
  14. $this->webstream = $webstream;
  15. }
  16. }
  17. public function getOrm()
  18. {
  19. return $this->webstream;
  20. }
  21. public function getName()
  22. {
  23. return $this->webstream->getDbName();
  24. }
  25. public function getId()
  26. {
  27. return $this->webstream->getDbId();
  28. }
  29. public function getCreatorId()
  30. {
  31. return $this->webstream->getDbCreatorId();
  32. }
  33. public function getLastModified($p_type)
  34. {
  35. return $this->webstream->getDbMtime();
  36. }
  37. public function getDefaultLength()
  38. {
  39. $dateString = $this->webstream->getDbLength();
  40. $arr = explode(":", $dateString);
  41. if (count($arr) == 3) {
  42. list($hours, $min, $sec) = $arr;
  43. $di = new DateInterval("PT{$hours}H{$min}M{$sec}S");
  44. return $di->format("%Hh %Im");
  45. }
  46. return "";
  47. }
  48. public function getLength()
  49. {
  50. return $this->getDefaultLength();
  51. }
  52. public function getDescription()
  53. {
  54. return $this->webstream->getDbDescription();
  55. }
  56. public function getUrl()
  57. {
  58. return $this->webstream->getDbUrl();
  59. }
  60. public function getMetadata()
  61. {
  62. $subjs = CcSubjsQuery::create()->findPK($this->webstream->getDbCreatorId());
  63. $username = $subjs->getDbLogin();
  64. return array(
  65. "name" => $this->webstream->getDbName(),
  66. "length" => $this->webstream->getDbLength(),
  67. "description" => $this->webstream->getDbDescription(),
  68. "login" => $username,
  69. "url" => $this->webstream->getDbUrl(),
  70. );
  71. }
  72. public static function deleteStreams($p_ids, $p_userId)
  73. {
  74. $userInfo = Zend_Auth::getInstance()->getStorage()->read();
  75. $user = new Application_Model_User($userInfo->id);
  76. $isAdminOrPM = $user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER));
  77. if (!$isAdminOrPM) {
  78. //Make sure the user has ownership of ALL the selected webstreams before
  79. $leftOver = self::streamsNotOwnedByUser($p_ids, $p_userId);
  80. if (count($leftOver) == 0) {
  81. CcWebstreamQuery::create()->findPKs($p_ids)->delete();
  82. } else {
  83. throw new WebstreamNoPermissionException;
  84. }
  85. } else {
  86. CcWebstreamQuery::create()->findPKs($p_ids)->delete();
  87. }
  88. }
  89. // This function returns that are not owen by $p_user_id among $p_ids
  90. private static function streamsNotOwnedByUser($p_ids, $p_userId)
  91. {
  92. $ownedByUser = CcWebstreamQuery::create()->filterByDbCreatorId($p_userId)->find()->getData();
  93. $ownedStreams = array();
  94. foreach ($ownedByUser as $pl) {
  95. if (in_array($pl->getDbId(), $p_ids)) {
  96. $ownedStreams[] = $pl->getDbId();
  97. }
  98. }
  99. $leftOvers = array_diff($p_ids, $ownedStreams);
  100. return $leftOvers;
  101. }
  102. public static function analyzeFormData($parameters)
  103. {
  104. $valid = array("length" => array(true, ''),
  105. "url" => array(true, ''),
  106. "name" => array(true, ''));
  107. $di = null;
  108. $length = $parameters["length"];
  109. $result = preg_match("/^(?:([0-9]{1,2})h)?\s*(?:([0-9]{1,2})m)?$/", $length, $matches);
  110. $invalid_date_interval = false;
  111. if ($result == 1 && count($matches) == 2) {
  112. $hours = $matches[1];
  113. $minutes = 0;
  114. } elseif ($result == 1 && count($matches) == 3) {
  115. $hours = $matches[1];
  116. $minutes = $matches[2];
  117. } else {
  118. $invalid_date_interval = true;
  119. }
  120. if (!$invalid_date_interval) {
  121. //Due to the way our Regular Expression is set up, we could have $minutes or $hours
  122. //not set. Do simple test here
  123. if (!is_numeric($hours)) {
  124. $hours = 0;
  125. }
  126. if (!is_numeric($minutes)) {
  127. $minutes = 0;
  128. }
  129. //minutes cannot be over 59. Need to convert anything > 59 minutes into hours.
  130. $hours += intval($minutes/60);
  131. $minutes = $minutes%60;
  132. $di = new DateInterval("PT{$hours}H{$minutes}M");
  133. $totalMinutes = $di->h * 60 + $di->i;
  134. if ($totalMinutes == 0) {
  135. $valid['length'][0] = false;
  136. $valid['length'][1] = _('Length needs to be greater than 0 minutes');
  137. }
  138. } else {
  139. $valid['length'][0] = false;
  140. $valid['length'][1] = _('Length should be of form "00h 00m"');
  141. }
  142. $url = $parameters["url"];
  143. //simple validator that checks to make sure that the url starts with
  144. //http(s),
  145. //and that the domain is at least 1 letter long
  146. $result = preg_match("/^(http|https):\/\/.+/", $url, $matches);
  147. $mime = null;
  148. $mediaUrl = null;
  149. if ($result == 0) {
  150. $valid['url'][0] = false;
  151. $valid['url'][1] = _('URL should be of form "http://domain"');
  152. } elseif (strlen($url) > 512) {
  153. $valid['url'][0] = false;
  154. $valid['url'][1] = _('URL should be 512 characters or less');
  155. } else {
  156. try {
  157. list($mime, $content_length_found) = self::discoverStreamMime($url);
  158. if (is_null($mime)) {
  159. throw new Exception(_("No MIME type found for webstream."));
  160. }
  161. $mediaUrl = self::getMediaUrl($url, $mime, $content_length_found);
  162. if (preg_match("/(x-mpegurl)|(xspf\+xml)|(pls\+xml)|(x-scpls)/", $mime)) {
  163. list($mime, $content_length_found) = self::discoverStreamMime($mediaUrl);
  164. }
  165. } catch (Exception $e) {
  166. $valid['url'][0] = false;
  167. $valid['url'][1] = $e->getMessage();
  168. }
  169. }
  170. $name = $parameters["name"];
  171. if (strlen($name) == 0) {
  172. $valid['name'][0] = false;
  173. $valid['name'][1] = _('Webstream name cannot be empty');
  174. }
  175. $id = $parameters["id"];
  176. return array($valid, $mime, $mediaUrl, $di);
  177. }
  178. public static function isValid($analysis)
  179. {
  180. foreach ($analysis as $k => $v) {
  181. if ($v[0] === false) {
  182. return false;
  183. }
  184. }
  185. return true;
  186. }
  187. // TODO : Fix this interface
  188. //This function should not be defined in the interface.
  189. public function setMetadata($key, $val)
  190. {
  191. throw new Exception("Not implemented.");
  192. }
  193. public function setName($name)
  194. {
  195. $this->webstream->setDbName($name);
  196. }
  197. public function setLastPlayed($timestamp)
  198. {
  199. $this->webstream->setDbLPtime($timestamp);
  200. $this->webstream->save();
  201. }
  202. private static function getUrlData($url)
  203. {
  204. $ch = curl_init();
  205. curl_setopt($ch, CURLOPT_URL, $url);
  206. curl_setopt($ch, CURLOPT_HEADER, 0);
  207. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  208. // grab URL and pass it to the browser
  209. //TODO: What if invalid url?
  210. $content = curl_exec($ch);
  211. // close cURL resource, and free up system resources
  212. curl_close($ch);
  213. return $content;
  214. }
  215. private static function getXspfUrl($url)
  216. {
  217. $content = self::getUrlData($url);
  218. $dom = new DOMDocument;
  219. //TODO: What if invalid xml?
  220. $dom->loadXML($content);
  221. $tracks = $dom->getElementsByTagName('track');
  222. foreach ($tracks as $track) {
  223. $locations = $track->getElementsByTagName('location');
  224. foreach ($locations as $loc) {
  225. return $loc->nodeValue;
  226. }
  227. }
  228. throw new Exception(_("Could not parse XSPF playlist"));
  229. }
  230. private static function getPlsUrl($url)
  231. {
  232. $content = self::getUrlData($url);
  233. $matches = array();
  234. $numStreams = 0; //Number of streams explicitly listed in the PLS.
  235. if (preg_match("/NumberOfEntries=([0-9]*)/", $content, $matches) !== FALSE) {
  236. $numStreams = $matches[1];
  237. }
  238. //Find all the stream URLs in the playlist
  239. if (preg_match_all("/File[0-9]*=(.*)/", $content, $matches) !== FALSE) {
  240. //This array contains all the streams! If we need fallback stream URLs in the future,
  241. //they're already in this array...
  242. return $matches[1][0];
  243. } else {
  244. throw new Exception(_("Could not parse PLS playlist"));
  245. }
  246. }
  247. private static function getM3uUrl($url)
  248. {
  249. $content = self::getUrlData($url);
  250. //split into lines:
  251. $delim = "\n";
  252. if (strpos($content, "\r\n") !== false) {
  253. $delim = "\r\n";
  254. }
  255. $lines = explode("$delim", $content);
  256. #$lines = preg_split('/$\R?^/m', $content);
  257. if (count($lines) > 0) {
  258. return $lines[0];
  259. }
  260. throw new Exception(_("Could not parse M3U playlist"));
  261. }
  262. private static function getMediaUrl($url, $mime, $content_length_found)
  263. {
  264. if (preg_match("/x-mpegurl/", $mime)) {
  265. $media_url = self::getM3uUrl($url);
  266. } elseif (preg_match("/xspf\+xml/", $mime)) {
  267. $media_url = self::getXspfUrl($url);
  268. } elseif (preg_match("/pls\+xml/", $mime) || preg_match("/x-scpls/", $mime)) {
  269. $media_url = self::getPlsUrl($url);
  270. } elseif (preg_match("/(mpeg|ogg|audio\/aacp|audio\/aac)/", $mime)) {
  271. if ($content_length_found) {
  272. throw new Exception(_("Invalid webstream - This appears to be a file download."));
  273. }
  274. $media_url = $url;
  275. } else {
  276. throw new Exception(sprintf(_("Unrecognized stream type: %s"), $mime));
  277. }
  278. return $media_url;
  279. }
  280. /* PHP get_headers has an annoying property where if the passed in URL is
  281. * a redirect, then it goes to the new URL, and returns headers from both
  282. * requests. We only want the headers from the final request. Here's an
  283. * example:
  284. *
  285. * 0 => "HTTP/1.1 302 Moved Temporarily",
  286. * 1 => "X-Powered-By: Servlet/3.0 JSP/2.2 (GlassFish Server Open Source Edition 3.1.1 Java/Sun Microsystems Inc./1.6)",
  287. * 2 => "Server: GlassFish Server Open Source Edition 3.1.1",
  288. * 3 => "Location: http://3043.live.streamtheworld.com:80/SAM04AAC89_SC",
  289. * 4 => "Content-Type: text/html;charset=ISO-8859-1",
  290. * 5 => "Content-Language: en-US",
  291. * 6 => "Content-Length: 202",
  292. * 7 => "Date: Thu, 27 Dec 2012 21:52:59 GMT",
  293. * 8 => "Connection: close",
  294. * 9 => "HTTP/1.0 200 OK",
  295. * 10 => "Expires: Thu, 01 Dec 2003 16:00:00 GMT",
  296. * 11 => "Cache-Control: no-cache, must-revalidate",
  297. * 12 => "Pragma: no-cache",
  298. * 13 => "Content-Type: audio/aacp",
  299. * 14 => "icy-br: 68",
  300. * 15 => "Server: MediaGateway 3.2.1-04",
  301. * */
  302. private static function cleanHeaders($headers) {
  303. //find the position of HTTP/1 200 OK
  304. //
  305. $position = 0;
  306. foreach ($headers as $i => $v) {
  307. if (preg_match("/^HTTP.*200 OK$/i", $v)) {
  308. $position = $i;
  309. break;
  310. }
  311. }
  312. return array_slice($headers, $position);
  313. }
  314. private static function discoverStreamMime($url)
  315. {
  316. try {
  317. $headers = @get_headers($url);
  318. $mime = null;
  319. $content_length_found = false;
  320. if ($headers !== false) {
  321. $headers = self::cleanHeaders($headers);
  322. foreach ($headers as $h) {
  323. if (preg_match("/^content-type:/i", $h)) {
  324. list(, $value) = explode(":", $h, 2);
  325. $mime = trim($value);
  326. }
  327. if (preg_match("/^content-length:/i", $h)) {
  328. $content_length_found = true;
  329. }
  330. }
  331. }
  332. } catch (Exception $e) {
  333. Logging::info("Invalid stream URL");
  334. Logging::info($e->getMessage());
  335. }
  336. return array($mime, $content_length_found);
  337. }
  338. public static function save($parameters, $mime, $mediaUrl, $di)
  339. {
  340. $userInfo = Zend_Auth::getInstance()->getStorage()->read();
  341. $id = $parameters['id'];
  342. if ($id != -1) {
  343. $webstream = CcWebstreamQuery::create()->findPK($id);
  344. } else {
  345. $webstream = new CcWebstream();
  346. }
  347. $webstream->setDbName($parameters["name"]);
  348. $webstream->setDbDescription($parameters["description"]);
  349. $webstream->setDbUrl($mediaUrl);
  350. $dblength = $di->format("%H:%I");
  351. $webstream->setDbLength($dblength);
  352. $webstream->setDbCreatorId($userInfo->id);
  353. $webstream->setDbUtime(new DateTime("now", new DateTimeZone('UTC')));
  354. $webstream->setDbMtime(new DateTime("now", new DateTimeZone('UTC')));
  355. $ws = new Application_Model_Webstream($webstream);
  356. $webstream->setDbMime($mime);
  357. $webstream->save();
  358. return $webstream->getDbId();
  359. }
  360. /*
  361. * method is not used, webstreams aren't currently kept track of for isScheduled.
  362. */
  363. public static function setIsScheduled($p_webstreamId, $p_status) {
  364. $webstream = CcWebstreamQuery::create()->findPK($p_webstreamId);
  365. $updateIsScheduled = false;
  366. if (isset($webstream) && !in_array($p_webstreamId,
  367. Application_Model_Schedule::getAllFutureScheduledWebstreams())) {
  368. //$webstream->setDbIsScheduled($p_status)->save();
  369. $updateIsScheduled = true;
  370. }
  371. return $updateIsScheduled;
  372. }
  373. }
  374. class WebstreamNoPermissionException extends Exception {}