Favicon.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <?php
  2. namespace Favicon;
  3. class Favicon
  4. {
  5. protected $url = '';
  6. protected $cacheDir;
  7. protected $cacheTimeout;
  8. protected $dataAccess;
  9. public function __construct($args = array())
  10. {
  11. if (isset($args['url'])) {
  12. $this->url = $args['url'];
  13. }
  14. $this->cacheDir = __DIR__ . '/../../resources/cache';
  15. $this->dataAccess = new DataAccess();
  16. }
  17. public function cache($args = array()) {
  18. if (isset($args['dir'])) {
  19. $this->cacheDir = $args['dir'];
  20. }
  21. if (!empty($args['timeout'])) {
  22. $this->cacheTimeout = $args['timeout'];
  23. } else {
  24. $this->cacheTimeout = 0;
  25. }
  26. }
  27. public static function baseUrl($url, $path = false)
  28. {
  29. $return = '';
  30. if (!$url = parse_url($url)) {
  31. return FALSE;
  32. }
  33. // Scheme
  34. $scheme = isset($url['scheme']) ? strtolower($url['scheme']) : null;
  35. if ($scheme != 'http' && $scheme != 'https') {
  36. return FALSE;
  37. }
  38. $return .= "{$scheme}://";
  39. // Username and password
  40. if (isset($url['user'])) {
  41. $return .= $url['user'];
  42. if (isset($url['pass'])) {
  43. $return .= ":{$url['pass']}";
  44. }
  45. $return .= '@';
  46. }
  47. // Hostname
  48. if( !isset($url['host']) ) {
  49. return FALSE;
  50. }
  51. $return .= $url['host'];
  52. // Port
  53. if (isset($url['port'])) {
  54. $return .= ":{$url['port']}";
  55. }
  56. // Path
  57. if( $path && isset($url['path']) ) {
  58. $return .= $url['path'];
  59. }
  60. $return .= '/';
  61. return $return;
  62. }
  63. public function info($url)
  64. {
  65. if(empty($url) || $url === false) {
  66. return false;
  67. }
  68. $max_loop = 5;
  69. // Discover real status by following redirects.
  70. $loop = TRUE;
  71. while ($loop && $max_loop-- > 0) {
  72. $headers = $this->dataAccess->retrieveHeader($url);
  73. $exploded = explode(' ', $headers[0]);
  74. if( !isset($exploded[1]) ) {
  75. return false;
  76. }
  77. list(,$status) = $exploded;
  78. switch ($status) {
  79. case '301':
  80. case '302':
  81. $url = isset($headers['location']) ? $headers['location'] : '';
  82. break;
  83. default:
  84. $loop = FALSE;
  85. break;
  86. }
  87. }
  88. return array('status' => $status, 'url' => $url);
  89. }
  90. public function endRedirect($url) {
  91. $out = $this->info($url);
  92. return !empty($out['url']) ? $out['url'] : false;
  93. }
  94. /**
  95. * Find remote (or cached) favicon
  96. * @return favicon URL, false if nothing was found
  97. **/
  98. public function get($url = '')
  99. {
  100. // URLs passed to this method take precedence.
  101. if (!empty($url)) {
  102. $this->url = $url;
  103. }
  104. // Get the base URL without the path for clearer concatenations.
  105. $original = rtrim($this->baseUrl($this->url, true), '/');
  106. $url = rtrim($this->endRedirect($this->baseUrl($this->url, false)), '/');
  107. if(($favicon = $this->checkCache($url)) || ($favicon = $this->getFavicon($url))) {
  108. $base = true;
  109. }
  110. elseif(($favicon = $this->checkCache($original)) || ($favicon = $this->getFavicon($original, false))) {
  111. $base = false;
  112. }
  113. else
  114. return false;
  115. // Save cache if necessary
  116. $cache = $this->cacheDir . '/' . md5($base ? $url : $original);
  117. if ($this->cacheTimeout && !file_exists($cache) || (is_writable($cache) && time() - filemtime($cache) > $this->cacheTimeout)) {
  118. $this->dataAccess->saveCache($cache, $favicon);
  119. }
  120. return $favicon;
  121. }
  122. private function getFavicon($url, $checkDefault = true) {
  123. $favicon = false;
  124. if(empty($url)) {
  125. return false;
  126. }
  127. // Try /favicon.ico first.
  128. if( $checkDefault ) {
  129. $info = $this->info("{$url}/favicon.ico");
  130. if ($info['status'] == '200') {
  131. $favicon = $info['url'];
  132. }
  133. }
  134. // See if it's specified in a link tag in domain url.
  135. if (!$favicon) {
  136. $favicon = $this->getInPage($url);
  137. }
  138. // Make sure the favicon is an absolute URL.
  139. if( $favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false ) {
  140. $favicon = $url . '/' . $favicon;
  141. }
  142. // Sometimes people lie, so check the status.
  143. // And sometimes, it's not even an image. Sneaky bastards!
  144. // If cacheDir isn't writable, that's not our problem
  145. if ($favicon && is_writable($this->cacheDir) && !$this->checkImageMType($favicon)) {
  146. $favicon = false;
  147. }
  148. return $favicon;
  149. }
  150. private function getInPage($url) {
  151. $html = $this->dataAccess->retrieveUrl("{$url}/");
  152. preg_match('!<head.*?>.*</head>!ims', $html, $match);
  153. if(empty($match) || count($match) == 0) {
  154. return false;
  155. }
  156. $head = $match[0];
  157. $dom = new \DOMDocument();
  158. // Use error supression, because the HTML might be too malformed.
  159. if (@$dom->loadHTML($head)) {
  160. $links = $dom->getElementsByTagName('link');
  161. foreach ($links as $link) {
  162. if ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'shortcut icon') {
  163. return $link->getAttribute('href');
  164. } elseif ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'icon') {
  165. return $link->getAttribute('href');
  166. } elseif ($link->hasAttribute('href') && strpos($link->getAttribute('href'), 'favicon') !== FALSE) {
  167. return $link->getAttribute('href');
  168. }
  169. }
  170. }
  171. return false;
  172. }
  173. private function checkCache($url) {
  174. if ($this->cacheTimeout) {
  175. $cache = $this->cacheDir . '/' . md5($url);
  176. if (file_exists($cache) && is_readable($cache) && (time() - filemtime($cache) < $this->cacheTimeout)) {
  177. return $this->dataAccess->readCache($cache);
  178. }
  179. }
  180. return false;
  181. }
  182. private function checkImageMType($url) {
  183. $tmpFile = $this->cacheDir . '/tmp.ico';
  184. $fileContent = $this->dataAccess->retrieveUrl($url);
  185. $this->dataAccess->saveCache($tmpFile, $fileContent);
  186. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  187. $isImage = strpos(finfo_file($finfo, $tmpFile), 'image') !== false;
  188. finfo_close($finfo);
  189. unlink($tmpFile);
  190. return $isImage;
  191. }
  192. /**
  193. * @return mixed
  194. */
  195. public function getCacheDir()
  196. {
  197. return $this->cacheDir;
  198. }
  199. /**
  200. * @param mixed $cacheDir
  201. */
  202. public function setCacheDir($cacheDir)
  203. {
  204. $this->cacheDir = $cacheDir;
  205. }
  206. /**
  207. * @return mixed
  208. */
  209. public function getCacheTimeout()
  210. {
  211. return $this->cacheTimeout;
  212. }
  213. /**
  214. * @param mixed $cacheTimeout
  215. */
  216. public function setCacheTimeout($cacheTimeout)
  217. {
  218. $this->cacheTimeout = $cacheTimeout;
  219. }
  220. /**
  221. * @return string
  222. */
  223. public function getUrl()
  224. {
  225. return $this->url;
  226. }
  227. /**
  228. * @param string $url
  229. */
  230. public function setUrl($url)
  231. {
  232. $this->url = $url;
  233. }
  234. /**
  235. * @param DataAccess $dataAccess
  236. */
  237. public function setDataAccess($dataAccess)
  238. {
  239. $this->dataAccess = $dataAccess;
  240. }
  241. }