├── README.md └── videoframes └── init.php /README.md: -------------------------------------------------------------------------------- 1 | videoframes for tt-rss 2 | ====================== 3 | 4 | [Tiny Tiny RSS](http://www.tt-rss.org) plugin to **enable embedded videos** in feeds. 5 | 6 | It currently supports Youtube, Vimeo, Dailymotion, MyVideo, Viddler, SoundCloud and Spotify. If you are missing a site in this list, please [open a ticket](https://github.com/tribut/ttrss-videoframes/issues/new). 7 | 8 | ![Screenshot](http://i.imgur.com/MhccdQn.png) 9 | 10 | ## How does it work? 11 | This plugins allows the inclusion of iframes from the above listed sites without the *sandbox* attribute to enable flash videos. Additionally, the plugin will transform directly embedded videos (object/embed tags) from those sites to iframes so they can be shown as well. 12 | 13 | If you do not trust one or more of these sites this plugin could be considered a *security risk*. It will force the iframe to be requested over https to avoid possible MITM scenarios however. 14 | 15 | 16 | Requires **tt-rss 1.7.5 or later**. Note, that if your browser does not support the sandbox attribute, this plugin might not work on versions of tt-rss prior to 1.7.6. 17 | 18 | ## Installation 19 | 20 | * Unpack the [zip-File](https://github.com/tribut/ttrss-videoframes/archive/master.zip) 21 | * Move the folder "videoframes" to your plugins directory 22 | * Enable "videoframes" in TT-RSS's Preferences -> Plugins 23 | 24 | Please report any problems you might encounter using github's [issue tracker](https://github.com/tribut/ttrss-videoframes/issues). 25 | 26 | ## Legal 27 | 28 | Copyright [Felix Eckhofer](https://tribut.de) and Contributors 29 | 30 | > This program is free software: you can redistribute it and/or modify 31 | > it under the terms of the GNU General Public License as published by 32 | > the Free Software Foundation, either version 3 of the License, or 33 | > (at your option) any later version. 34 | > 35 | > This program is distributed in the hope that it will be useful, 36 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | > GNU General Public License for more details. 39 | > 40 | > You should have received a copy of the GNU General Public License 41 | > along with this program. If not, see . 42 | -------------------------------------------------------------------------------- /videoframes/init.php: -------------------------------------------------------------------------------- 1 | '/embed/', 13 | 'www.youtube-nocookie.com' => '/embed/', 14 | 'player.vimeo.com' => '/video/', 15 | 'www.myvideo.de' => '/embed/', 16 | 'www.dailymotion.com' => '/embed/video/', 17 | 'www.viddler.com' => '/embed/', 18 | 'w.soundcloud.com' => '/player/', 19 | 'www.facebook.com' => '/video/embed', 20 | 'www.ustream.tv' => '/embed/', 21 | 'open.spotify.com' => '/embed/' 22 | ); 23 | 24 | /** 25 | * Array of style embedded flash videos that should 26 | * be transformed to an iframe 27 | * 28 | * [key] full hostname in the src attribute 29 | * [value][0] regex that the path must match against or, if it starts with 30 | * a '?' name of the query string argument that is needed to 31 | * build the iframe src 32 | * [value][1] path of the iframe src, $1 is replaced by the first grouped 33 | * expression in the regex or the value of the query string 34 | * variable, respectively 35 | * [value][2] (optional) hostname for iframe src 36 | * 37 | * @var array 38 | */ 39 | protected $transform_objects = array( 40 | 'www.youtube.com' => array( 41 | '#^/v/([a-zA-Z0-9_]+)(&.*)?$#', 42 | '/embed/$1' 43 | ), 44 | 'www.youtube-nocookie-com' => array( 45 | '#^/v/([a-zA-Z0-9_]+)(&.*)?$#', 46 | '/embed/$1' 47 | ), 48 | 'vimeo.com' => array( 49 | '?clip_id', 50 | '/video/$1', 51 | 'player.vimeo.com' 52 | ), 53 | 'www.myvideo.de' => array( 54 | '#^/movie/([a-zA-Z0-9_]+)(&.*)?$#', 55 | '/embed/$1' 56 | ), 57 | 'www.dailymotion.com' => array( 58 | '#^/swf/video/([a-zA-Z0-9_]+)(&.*)?$#', 59 | '#/embed/video/$1#' 60 | ) 61 | ); 62 | 63 | function about() { 64 | return array(0.4, 65 | "Enable the playback of embedded videos from well-known sites", 66 | "dxbi", 67 | false); 68 | } 69 | 70 | function api_version() { 71 | return 2; 72 | } 73 | 74 | function init($host) { 75 | $host->add_hook($host::HOOK_SANITIZE, $this); 76 | } 77 | 78 | function hook_sanitize($doc, $site_url, $allowed_elements = null, $disallowed_attributes = null, $article_id) { 79 | $remove_unknown_iframes = false; 80 | if (!is_null($allowed_elements) && !is_null($disallowed_attributes)) { 81 | if (!array_search('iframe', $allowed_elements)) { 82 | $remove_unknown_iframes = true; 83 | $allowed_elements[] = 'iframe'; 84 | } 85 | } 86 | 87 | $xpath = new DOMXPath($doc); 88 | 89 | // remove sandbox from whitelisted iframes and force https 90 | $entries = $xpath->query('//iframe'); 91 | foreach ($entries as $entry) { 92 | $src = $this->_getSrcAttribute($entry); 93 | $url = $this->_parseUrl($src); 94 | 95 | if ($this->_isIframeUrlValid($url)) { 96 | // force https 97 | // http_build_url would be the nice solution, 98 | // but that's apparently not available everywhere 99 | $src = preg_replace('#^[a-z]+://#i', 'https://', 100 | $src, -1, $rcount); 101 | if ($rcount < 1) { // if this happens the url is really strange 102 | continue; 103 | } 104 | $entry->setAttribute('src', $src); 105 | 106 | $entry = $this->_removeSandboxAttribute($entry); 107 | } elseif ($remove_unknown_iframes) { 108 | $entry->parentNode->removeChild($entry); 109 | } 110 | } 111 | 112 | // replace style flash videos with iframe 113 | $entries = $xpath->query('//object/embed[@src]'); 114 | foreach ($entries as $entry) { 115 | $src = $this->_getSrcAttribute($entry); 116 | $url = $this->_parseUrl($src); 117 | if (!$url) { 118 | continue; 119 | } 120 | 121 | $host = $url['host']; 122 | if (array_key_exists($host, $this->transform_objects)) { 123 | $pattern = $this->transform_objects[$host][0]; 124 | $replace = $this->transform_objects[$host][1]; 125 | if (isset($this->transform_objects[$host][2])) { 126 | $newhost = $this->transform_objects[$host][2]; 127 | } else { 128 | $newhost = $host; 129 | } 130 | if ($pattern[0] == '?') { 131 | $querykey = substr($pattern, 1); 132 | parse_str($url['query'], $query); 133 | if (array_key_exists($querykey, $query) && 134 | preg_match('/^[a-zA-Z0-9_]+$/', $query[$querykey])) { 135 | $iframesrc = 'https://' . $newhost . 136 | str_replace('$1', 137 | $query[$querykey], 138 | $replace 139 | ); 140 | } else { // query string parameter not set 141 | continue; 142 | } 143 | } else { // not a query string, use path 144 | $iframesrc = 'https://' . $newhost . 145 | preg_replace($pattern, 146 | $replace, 147 | $url['path'], 148 | -1, $rcount); 149 | if ($rcount < 1) { 150 | continue; 151 | } 152 | } 153 | } else { // host not in whitelist 154 | continue; 155 | } 156 | 157 | $tag_object = $entry->parentNode; 158 | $tag_parent = $tag_object->parentNode; 159 | $height = intval($entry->getAttribute('height')); 160 | $width = intval($entry->getAttribute('width')); 161 | // youtube defaults 162 | if ($height < 1) { 163 | $height = 315; 164 | } 165 | if ($width < 1) { 166 | $width = 560; 167 | } 168 | 169 | $tag_iframe = $doc->createElement('iframe'); 170 | $tag_iframe->setAttribute('allowfullscreen', ''); 171 | $tag_iframe->setAttribute('width', $width); 172 | $tag_iframe->setAttribute('height', $height); 173 | $tag_iframe->setAttribute('frameborder', '0'); 174 | $tag_iframe->setAttribute('src', $iframesrc); 175 | 176 | $tag_parent->replaceChild($tag_iframe, $tag_object); 177 | } 178 | 179 | if ($remove_unknown_iframes) { 180 | return array($doc, $allowed_elements, $disallowed_attributes); 181 | } else { 182 | return $doc; 183 | } 184 | } 185 | 186 | protected function _removeSandboxAttribute(DOMNode $node) 187 | { 188 | while ($node->hasAttribute('sandbox')) { 189 | $node->removeAttribute('sandbox'); 190 | } 191 | 192 | return $node; 193 | } 194 | 195 | protected function _getSrcAttribute(DOMNode $node) 196 | { 197 | $src = $node->getAttribute('src'); 198 | // unfortunately parse_url won't support urls without protocol 199 | // (albeit apparently allowed by the RFC...) 200 | if (strpos($src, '//') === 0) { 201 | $src = 'https:' . $src; 202 | } 203 | 204 | return $src; 205 | } 206 | 207 | /** 208 | * Parse a URL and return its components. Behaves like parse_url() except it 209 | * will always return false if no host-part is found. When is returns 210 | * something other than false, the keys 'host', 'path', 'query' will always 211 | * be set. 212 | */ 213 | protected function _parseUrl($src) 214 | { 215 | $url = parse_url($src); 216 | 217 | if (array_key_exists('host', $url)) { 218 | if (!array_key_exists('query', $url)) $url['query'] = ''; 219 | if (!array_key_exists('path', $url)) $url['path'] = ''; 220 | return $url; 221 | } else { 222 | return false; 223 | } 224 | } 225 | 226 | /** 227 | * Checks to see if the URL was parse-able, is in the allowed host list, and 228 | * begins with the proper path 229 | * 230 | * @param array|false $url Output of parse_url 231 | * 232 | * @return bool 233 | */ 234 | protected function _isIframeUrlValid($url) 235 | { 236 | return $url && 237 | array_key_exists($url['host'], $this->allowed_iframes) && 238 | strpos($url['path'], $this->allowed_iframes[$url['host']]) === 0; 239 | } 240 | } 241 | --------------------------------------------------------------------------------