├── .gitignore ├── assets ├── images │ ├── page.png │ ├── pageEnd.png │ ├── doogleLogo.png │ ├── pageStart.png │ ├── icons │ │ └── search.png │ ├── pageSelected.png │ └── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ └── android-chrome-512x512.png ├── js │ ├── script.js │ ├── masonry │ │ └── 4.2.2 │ │ │ └── masonry.pkgd.min.js │ └── fancybox │ │ └── 3.3.5 │ │ └── jquery.fancybox.min.js └── css │ ├── style.css │ └── fancybox │ └── 3.3.5 │ └── jquery.fancybox.min.css ├── ajax ├── setBroken.php ├── updateLinkCount.php └── updateImageCount.php ├── config.php ├── classes ├── DomDocumentParser.php ├── ImageResultsProvider.php ├── SiteResultsProvider.php └── Crawler.php ├── LICENSE ├── index.php ├── doogle-tables-no-data.sql ├── search.php ├── crawl-manual.php ├── crawl.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | config.php 3 | -------------------------------------------------------------------------------- /assets/images/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/page.png -------------------------------------------------------------------------------- /assets/images/pageEnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/pageEnd.png -------------------------------------------------------------------------------- /assets/images/doogleLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/doogleLogo.png -------------------------------------------------------------------------------- /assets/images/pageStart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/pageStart.png -------------------------------------------------------------------------------- /assets/images/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/icons/search.png -------------------------------------------------------------------------------- /assets/images/pageSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/pageSelected.png -------------------------------------------------------------------------------- /assets/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/favicon.ico -------------------------------------------------------------------------------- /assets/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safesploitOrg/doogle/HEAD/assets/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /ajax/setBroken.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE images SET broken = 1 WHERE imageUrl=:src"); 7 | $query->bindParam(":src", $_POST["src"]); 8 | 9 | $query->execute(); 10 | } 11 | else 12 | echo "No src passed to page"; //DEBUGGING 13 | ?> -------------------------------------------------------------------------------- /ajax/updateLinkCount.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE sites SET clicks = clicks + 1 WHERE id=:id"); 7 | $query->bindParam(":id", $_POST["linkId"]); 8 | 9 | $query->execute(); 10 | } 11 | else 12 | echo "No link passed to page"; //DEBUGGING 13 | ?> -------------------------------------------------------------------------------- /ajax/updateImageCount.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE images SET clicks = clicks + 1 WHERE imageUrl=:imageUrl"); 7 | $query->bindParam(":imageUrl", $_POST["imageUrl"]); 8 | 9 | $query->execute(); 10 | } 11 | else 12 | echo "No image URL passed to page"; //DEBUGGING 13 | ?> -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); 13 | } 14 | catch(PDOExeption $e) 15 | { 16 | echo "Connection failed: " . $e->getMessage(); 17 | } 18 | ?> 19 | -------------------------------------------------------------------------------- /classes/DomDocumentParser.php: -------------------------------------------------------------------------------- 1 | '; 9 | 10 | $options = array( 11 | 'http'=>array('method'=>"GET", 'header'=>"User-Agent: doogleBot/0.1\n") 12 | ); 13 | $context = stream_context_create($options); 14 | $getConstants = file_get_contents($url, false, $context); 15 | 16 | $this->doc = new DomDocument('1.0', 'utf-8'); 17 | @$this->doc->loadHTML($html . $getConstants); 18 | //@ Error supression is unnecessary, PHP>7.0 supports HTML5 19 | } 20 | 21 | public function getlinks() 22 | { 23 | return $this->doc->getElementsByTagName("a"); 24 | } 25 | 26 | public function getTitleTags() 27 | { 28 | return $this->doc->getElementsByTagName("title"); 29 | } 30 | 31 | public function getMetaTags() 32 | { 33 | return $this->doc->getElementsByTagName("meta"); 34 | } 35 | 36 | public function getImages() 37 | { 38 | return $this->doc->getElementsByTagName("img"); 39 | } 40 | 41 | } 42 | ?> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zepher Ashe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Doogle Web Crawler 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | Site logo 23 |
24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /classes/ImageResultsProvider.php: -------------------------------------------------------------------------------- 1 | con = $con; 9 | } 10 | 11 | public function getNumResults($term) 12 | { 13 | $query = $this->con->prepare("SELECT COUNT(*) as total 14 | FROM images 15 | WHERE (title LIKE :term 16 | OR alt LIKE :term) 17 | AND broken=0"); 18 | 19 | $searchTerm = "%". $term . "%"; 20 | $query->bindParam(":term", $searchTerm); 21 | $query->execute(); 22 | 23 | $row = $query->fetch(PDO::FETCH_ASSOC); 24 | return $row["total"]; 25 | } 26 | 27 | public function getResultsHtml($page, $pageSize, $term) 28 | { 29 | $fromLimit = ($page - 1) * $pageSize; 30 | 31 | $query = $this->con->prepare("SELECT * 32 | FROM images 33 | WHERE (title LIKE :term 34 | OR alt LIKE :term) 35 | AND broken=0 36 | ORDER BY clicks DESC 37 | LIMIT :fromLimit, :pageSize"); 38 | 39 | $searchTerm = "%". $term . "%"; 40 | $query->bindParam(":term", $searchTerm); 41 | $query->bindParam(":fromLimit", $fromLimit, PDO::PARAM_INT); 42 | $query->bindParam(":pageSize", $pageSize, PDO::PARAM_INT); 43 | $query->execute(); 44 | 45 | $resultsHtml = "
"; 46 | 47 | $count = 0; 48 | while($row = $query->fetch(PDO::FETCH_ASSOC)) 49 | { 50 | $count++; 51 | $id = $row["id"]; 52 | $imageUrl = $row["imageUrl"]; 53 | $siteUrl = $row["siteUrl"]; 54 | $title = $row["title"]; 55 | $alt = $row["alt"]; 56 | 57 | if($title) 58 | $displayText = $title; 59 | else if($alt) 60 | $displayText = $alt; 61 | else 62 | $displayText = $imageUrl; 63 | 64 | $resultsHtml .= "
65 | 67 | 68 | 73 | 74 | $displayText 75 | 76 | 77 |
"; 78 | } 79 | 80 | $resultsHtml .= "
"; 81 | 82 | return $resultsHtml; 83 | } 84 | } 85 | ?> -------------------------------------------------------------------------------- /assets/js/script.js: -------------------------------------------------------------------------------- 1 | var timer; 2 | 3 | $(document).ready(function() { 4 | 5 | 6 | $(".result").on("click", function() { 7 | 8 | var id = $(this).attr("data-linkId"); 9 | var url = $(this).attr("href"); 10 | 11 | if(!id) { 12 | alert("data-linkId attribute not found"); //DEBUGGING 13 | } 14 | 15 | increaseLinkClicks(id, url); 16 | 17 | return false; 18 | }); 19 | 20 | 21 | var grid = $(".imageResults"); 22 | 23 | grid.on("layoutComplete", function() { 24 | $(".gridItem img").css("visibility", "visible"); 25 | }); 26 | 27 | grid.masonry({ 28 | itemSelector: ".gridItem", 29 | columnWidth: 200, 30 | gutter: 5, 31 | isInitLayout: false 32 | }); 33 | 34 | 35 | $("[data-fancybox]").fancybox({ 36 | 37 | caption : function( instance, item ) { 38 | var caption = $(this).data('caption') || ''; 39 | var siteUrl = $(this).data('siteurl') || ''; 40 | 41 | 42 | if ( item.type === 'image' ) { 43 | caption = (caption.length ? caption + '
' : '') 44 | + 'View image
' 45 | + 'Visit page'; 46 | } 47 | 48 | return caption; 49 | }, 50 | afterShow : function( instance, item ) { 51 | increaseImageClicks(item.src); 52 | } 53 | 54 | 55 | }); 56 | 57 | }); 58 | 59 | function loadImage(src, className) { 60 | 61 | var image = $(""); 62 | 63 | image.on("load", function() { 64 | $("." + className + " a").append(image); 65 | 66 | clearTimeout(timer); 67 | 68 | timer = setTimeout(function() { 69 | $(".imageResults").masonry(); 70 | }, 200); 71 | 72 | }); 73 | 74 | image.on("error", function() { 75 | 76 | $("." + className).remove(); 77 | 78 | $.post("ajax/setBroken.php", {src: src}); 79 | 80 | }); 81 | 82 | image.attr("src", src); 83 | 84 | } 85 | 86 | 87 | function increaseLinkClicks(linkId, url) { 88 | 89 | $.post("ajax/updateLinkCount.php", {linkId: linkId}) 90 | .done(function(result) { 91 | if(result != "") { 92 | alert(result); 93 | return; 94 | } 95 | 96 | window.location.href = url; 97 | }); 98 | 99 | } 100 | 101 | function increaseImageClicks(imageUrl) { 102 | 103 | $.post("ajax/updateImageCount.php", {imageUrl: imageUrl}) 104 | .done(function(result) { 105 | if(result != "") { 106 | alert(result); 107 | return; 108 | } 109 | }); 110 | 111 | } -------------------------------------------------------------------------------- /classes/SiteResultsProvider.php: -------------------------------------------------------------------------------- 1 | con = $con; 9 | } 10 | 11 | public function getNumResults($term) 12 | { 13 | $query = $this->con->prepare("SELECT COUNT(*) as total 14 | FROM sites WHERE title LIKE :term 15 | OR url LIKE :term 16 | OR keywords LIKE :term 17 | OR description LIKE :term"); 18 | 19 | $searchTerm = "%". $term . "%"; 20 | $query->bindParam(":term", $searchTerm); 21 | $query->execute(); 22 | 23 | $row = $query->fetch(PDO::FETCH_ASSOC); 24 | return $row["total"]; 25 | } 26 | 27 | public function getResultsHtml($page, $pageSize, $term) 28 | { 29 | /* 30 | Pagination system logic ($fromLimit) 31 | page1: (1 - 1) * 20 = 0 32 | page2: (2 - 1) * 20 = 20 33 | page3: (3 - 1) * 20 = 40 34 | ... 35 | */ 36 | $fromLimit = ($page - 1) * $pageSize; 37 | 38 | $query = $this->con->prepare("SELECT * 39 | FROM sites WHERE title LIKE :term 40 | OR url LIKE :term 41 | OR keywords LIKE :term 42 | OR description LIKE :term 43 | ORDER BY clicks DESC 44 | LIMIT :fromLimit, :pageSize"); 45 | 46 | $searchTerm = "%". $term . "%"; 47 | $query->bindParam(":term", $searchTerm); 48 | $query->bindParam(":fromLimit", $fromLimit, PDO::PARAM_INT); 49 | $query->bindParam(":pageSize", $pageSize, PDO::PARAM_INT); 50 | $query->execute(); 51 | 52 | $resultsHtml = "
"; 53 | 54 | while($row = $query->fetch(PDO::FETCH_ASSOC)) 55 | { 56 | $id = $row["id"]; 57 | $url = $row["url"]; 58 | $title = $row["title"]; 59 | $description = $row["description"]; 60 | 61 | $title = $this->trimField($title, 55); 62 | $description = $this->trimField($description, 230); 63 | 64 | $resultsHtml .= "
65 |

66 | 67 | $title 68 | 69 |

70 | $url 71 | $description 72 |
"; 73 | } 74 | 75 | $resultsHtml .= "
"; 76 | 77 | return $resultsHtml; 78 | } 79 | 80 | private function trimField($string, $characterLimit) 81 | { 82 | $dots = strlen($string) > $characterLimit ? "..." : ""; 83 | return substr($string, 0, $characterLimit) . $dots; 84 | } 85 | } 86 | ?> -------------------------------------------------------------------------------- /doogle-tables-no-data.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump - No Data 2 | 3 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 4 | SET AUTOCOMMIT = 0; 5 | START TRANSACTION; 6 | SET time_zone = "+00:00"; 7 | 8 | 9 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 10 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 11 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 12 | /*!40101 SET NAMES utf8mb4 */; 13 | 14 | -- 15 | -- User Creation: `doogle` 16 | -- 17 | CREATE USER IF NOT EXISTS 'doogle'@'%' IDENTIFIED BY 'PASSWORD_HERE'; 18 | GRANT SELECT, INSERT, UPDATE ON `doogle`.* TO 'doogle'@'%'; 19 | 20 | -- 21 | -- Database: `doogle` 22 | -- 23 | CREATE DATABASE IF NOT EXISTS `doogle` DEFAULT CHARACTER SET utf8mb4; 24 | USE `doogle`; 25 | 26 | -- -------------------------------------------------------- 27 | 28 | -- 29 | -- Table structure for table `images` 30 | -- 31 | 32 | CREATE TABLE IF NOT EXISTS `images` ( 33 | `id` int(11) NOT NULL, 34 | `siteUrl` varchar(512) NOT NULL, 35 | `imageUrl` varchar(512) NOT NULL, 36 | `alt` varchar(512) NOT NULL, 37 | `title` varchar(512) NOT NULL, 38 | `clicks` int(11) NOT NULL DEFAULT '0', 39 | `broken` tinyint(4) NOT NULL DEFAULT '0' 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 41 | 42 | -- -------------------------------------------------------- 43 | 44 | -- 45 | -- Table structure for table `sites` 46 | -- 47 | 48 | CREATE TABLE IF NOT EXISTS `sites` ( 49 | `id` int(11) NOT NULL, 50 | `url` varchar(512) NOT NULL, 51 | `title` varchar(512) NOT NULL, 52 | `description` varchar(512) NOT NULL, 53 | `keywords` varchar(512) NOT NULL, 54 | `clicks` int(11) NOT NULL DEFAULT '0' 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 56 | 57 | -- -------------------------------------------------------- 58 | 59 | -- 60 | -- Table structure for table `users` 61 | -- 62 | 63 | CREATE TABLE IF NOT EXISTS `users` ( 64 | `id` int(11) NOT NULL, 65 | `username` varchar(100) NOT NULL, 66 | `email` varchar(150) NOT NULL, 67 | `password` varchar(255) NOT NULL 68 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 69 | 70 | 71 | -- 72 | -- Indexes for dumped tables 73 | -- 74 | 75 | -- 76 | -- Indexes for table `images` 77 | -- 78 | ALTER TABLE `images` 79 | ADD PRIMARY KEY (`id`); 80 | 81 | -- 82 | -- Indexes for table `sites` 83 | -- 84 | ALTER TABLE `sites` 85 | ADD PRIMARY KEY (`id`); 86 | 87 | -- 88 | -- Indexes for table `users` 89 | -- 90 | ALTER TABLE `users` 91 | ADD PRIMARY KEY (`id`); 92 | 93 | -- 94 | -- AUTO_INCREMENT for dumped tables 95 | -- 96 | 97 | -- 98 | -- AUTO_INCREMENT for table `images` 99 | -- 100 | ALTER TABLE `images` 101 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13003; 102 | 103 | -- 104 | -- AUTO_INCREMENT for table `sites` 105 | -- 106 | ALTER TABLE `sites` 107 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5297; 108 | 109 | -- 110 | -- AUTO_INCREMENT for table `users` 111 | -- 112 | ALTER TABLE `users` 113 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1382; 114 | COMMIT; 115 | 116 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 117 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 118 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 119 | -------------------------------------------------------------------------------- /search.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | <?php if(isset($term) && $term != '') echo($term . ' | '); ?>Doogle Search 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 | 54 | 57 |
58 |
59 |
60 |
61 | 62 |
63 | 75 |
76 |
77 | 78 |
79 | getNumResults($term); 92 | 93 | echo "

$numResults results found

"; 94 | echo $resultsProvider->getResultsHtml($page, $pageSize, $term); 95 | ?> 96 |
97 | 98 |
99 |
100 |
101 | 102 |
103 | 104 | $numPages + 1) 115 | $currentPage = $numPages + 1 - $pagesLeft; 116 | 117 | while($pagesLeft != 0 && $currentPage <= $numPages) 118 | { 119 | if($currentPage == $page) 120 | { 121 | echo "
122 | 123 | $currentPage 124 |
"; 125 | } 126 | else 127 | { 128 | echo ""; 134 | } 135 | 136 | $currentPage++; 137 | $pagesLeft--; 138 | } 139 | ?> 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 |
148 |
149 | 150 | 151 | 152 | 156 | 157 | -------------------------------------------------------------------------------- /crawl-manual.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT * FROM sites WHERE url = :url"); 15 | 16 | $query->bindParam(":url", $url); 17 | $query->execute(); 18 | 19 | return $query->rowCount() != 0; 20 | } 21 | 22 | function imageExists($src) 23 | { 24 | global $con; 25 | 26 | $query = $con->prepare("SELECT * FROM images WHERE imageUrl = :src"); 27 | 28 | $query->bindParam(":src", $src); 29 | $query->execute(); 30 | 31 | return $query->rowCount() != 0; 32 | } 33 | 34 | 35 | function insertLink($url, $title, $description, $keywords) 36 | { 37 | global $con; 38 | 39 | $query = $con->prepare("INSERT INTO sites(url, title, description, keywords) 40 | VALUES(:url, :title, :description, :keywords)"); 41 | 42 | $query->bindParam(":url", $url); 43 | $query->bindParam(":title", $title); 44 | $query->bindParam(":description", $description); 45 | $query->bindParam(":keywords", $keywords); 46 | 47 | return $query->execute(); 48 | } 49 | 50 | function insertImage($url, $src, $alt, $title) 51 | { 52 | global $con; 53 | 54 | $query = $con->prepare("INSERT INTO images(siteUrl, imageUrl, alt, title) 55 | VALUES(:siteUrl, :imageUrl, :alt, :title)"); 56 | 57 | $query->bindParam(":siteUrl", $url); 58 | $query->bindParam(":imageUrl", $src); 59 | $query->bindParam(":alt", $alt); 60 | $query->bindParam(":title", $title); 61 | 62 | return $query->execute(); 63 | } 64 | 65 | /* Converts relative link to absolute link */ 66 | function createLink($src, $url) 67 | { 68 | $scheme = parse_url($url)["scheme"]; // http 69 | $host = parse_url($url)["host"]; // www.safesploit.com 70 | 71 | if(substr($src, 0, 2) == "//") 72 | $src = $scheme . ":" . $src; 73 | else if(substr($src, 0, 1) == "/") 74 | $src = $scheme . "://" . $host . $src; 75 | else if(substr($src, 0, 2) == "./") 76 | $src = $scheme . "://" . $host . dirname(parse_url($url)["path"]) . substr($src, 1); 77 | else if(substr($src, 0, 3) == "../") 78 | $src = $scheme . "://" . $host . "/" . $src; 79 | else if(substr($src, 0, 5) != "https" && substr($src, 0, 4) != "http") 80 | $src = $scheme . "://" . $host . "/" . $src; 81 | 82 | return $src; 83 | } 84 | 85 | function getDetails($url) 86 | { 87 | global $alreadyFoundImages; 88 | 89 | $parser = new DomDocumentParser($url); 90 | 91 | $titleArray = $parser->getTitleTags(); 92 | 93 | if(sizeof($titleArray) == 0 || $titleArray->item(0) == NULL) 94 | return; 95 | 96 | //Replace linebreak 97 | $title = $titleArray->item(0)->nodeValue; 98 | $title = str_replace("\n", "", $title); 99 | 100 | //Return if no 101 | if($title == "") 102 | return; 103 | 104 | $description = ""; 105 | $keywords = ""; 106 | 107 | $metasArray = $parser->getMetatags(); 108 | 109 | foreach($metasArray as $meta) 110 | { 111 | if($meta->getAttribute("name") == "description") 112 | $description = $meta->getAttribute("content"); 113 | 114 | if($meta->getAttribute("name") == "keywords") 115 | $keywords = $meta->getAttribute("content"); 116 | } 117 | 118 | $description = str_replace("\n", "", $description); 119 | $keywords = str_replace("\n", "", $keywords); 120 | 121 | if(linkExists($url)) 122 | echo "$url already exists<br>"; 123 | else if(insertLink($url, $title, $description, $keywords)) 124 | echo "SUCCESS: $url<br>"; 125 | else 126 | echo "ERROR: Failed to insert $url<br>"; 127 | 128 | $imageArray = $parser->getImages(); 129 | foreach($imageArray as $image) 130 | { 131 | $src = $image->getAttribute("src"); 132 | $alt = $image->getAttribute("alt"); 133 | $title = $image->getAttribute("title"); 134 | 135 | if(!$title && !$alt) 136 | continue; 137 | 138 | $src = createLink($src, $url); 139 | 140 | if(!in_array($src, $alreadyFoundImages)) 141 | { 142 | $alreadyFoundImages[] = $src; 143 | 144 | if(imageExists($src)) 145 | echo "$src already exists<br>"; 146 | else if(insertImage($url, $src, $alt, $title)) 147 | echo "SUCCESS: $src<br>"; 148 | else 149 | echo "ERROR: Failed to insert $src<br>"; 150 | } 151 | 152 | } 153 | 154 | echo "<b>URL:</b> $url, <b>Title:</b> $title, <b>Description:</b> $description, <b>keywords:</b> $keywords<br>"; //DEBUGGING sites 155 | echo "<b>src:</b> <a href=$src>$src</a>, <b>alt:</b> $alt, <b>title:</b> $title, <b>url:</b> $url<br>"; //DEBUGGING images 156 | } 157 | 158 | function followLinks($url) 159 | { 160 | global $alreadyCrawled; 161 | global $crawling; 162 | 163 | $parser = new DomDocumentParser($url); 164 | 165 | $linkList = $parser->getLinks(); 166 | 167 | 168 | foreach($linkList as $link) 169 | { 170 | $href = $link->getAttribute("href"); 171 | 172 | // Filter hrefs 173 | if(strpos($href, "#") !== false) 174 | continue; 175 | else if(substr($href, 0, 11) == "javascript:") 176 | continue; 177 | 178 | $href = createLink($href, $url); 179 | 180 | if(!in_array($href, $alreadyCrawled)) 181 | { 182 | $alreadyCrawled[] = $href; 183 | $crawling[] = $href; 184 | 185 | getDetails($href); 186 | } 187 | //else return; //DEBUGGING 188 | 189 | echo ($href . "<br>"); //DEBUGGING 190 | } 191 | 192 | array_shift($crawling); 193 | 194 | foreach($crawling as $site) 195 | followLinks($site); 196 | } 197 | 198 | $startUrl = "https://thehackernews.com/"; 199 | followLinks($startUrl); 200 | 201 | ?> -------------------------------------------------------------------------------- /classes/Crawler.php: -------------------------------------------------------------------------------- 1 | <?php 2 | class Crawler 3 | { 4 | private $con; 5 | 6 | public function __construct($con) 7 | { 8 | $this->con = $con; 9 | } 10 | 11 | 12 | 13 | function linkExists($url) 14 | { 15 | global $con; 16 | 17 | $query = $con->prepare("SELECT * FROM sites WHERE url = :url"); 18 | 19 | $query->bindParam(":url", $url); 20 | $query->execute(); 21 | 22 | return $query->rowCount() != 0; 23 | } 24 | 25 | function imageExists($src) 26 | { 27 | global $con; 28 | 29 | $query = $con->prepare("SELECT * FROM images WHERE imageUrl = :src"); 30 | 31 | $query->bindParam(":src", $src); 32 | $query->execute(); 33 | 34 | return $query->rowCount() != 0; 35 | } 36 | 37 | 38 | function insertLink($url, $title, $description, $keywords) 39 | { 40 | global $con; 41 | 42 | $query = $con->prepare("INSERT INTO sites(url, title, description, keywords) 43 | VALUES(:url, :title, :description, :keywords)"); 44 | 45 | $query->bindParam(":url", $url); 46 | $query->bindParam(":title", $title); 47 | $query->bindParam(":description", $description); 48 | $query->bindParam(":keywords", $keywords); 49 | 50 | return $query->execute(); 51 | } 52 | 53 | function insertImage($url, $src, $alt, $title) 54 | { 55 | global $con; 56 | 57 | $query = $con->prepare("INSERT INTO images(siteUrl, imageUrl, alt, title) 58 | VALUES(:siteUrl, :imageUrl, :alt, :title)"); 59 | 60 | $query->bindParam(":siteUrl", $url); 61 | $query->bindParam(":imageUrl", $src); 62 | $query->bindParam(":alt", $alt); 63 | $query->bindParam(":title", $title); 64 | 65 | return $query->execute(); 66 | } 67 | 68 | /* Converts relative link to absolute link */ 69 | function createLink($src, $url) 70 | { 71 | $scheme = parse_url($url)["scheme"]; // http 72 | $host = parse_url($url)["host"]; // www.safesploit.com 73 | 74 | if(substr($src, 0, 2) == "//") 75 | $src = $scheme . ":" . $src; 76 | else if(substr($src, 0, 1) == "/") 77 | $src = $scheme . "://" . $host . $src; 78 | else if(substr($src, 0, 2) == "./") 79 | $src = $scheme . "://" . $host . dirname(parse_url($url)["path"]) . substr($src, 1); 80 | else if(substr($src, 0, 3) == "../") 81 | $src = $scheme . "://" . $host . "/" . $src; 82 | else if(substr($src, 0, 5) != "https" && substr($src, 0, 4) != "http") 83 | $src = $scheme . "://" . $host . "/" . $src; 84 | 85 | return $src; 86 | } 87 | 88 | function getDetails($url) 89 | { 90 | global $alreadyFoundImages; 91 | 92 | $parser = new DomDocumentParser($url); 93 | 94 | $titleArray = $parser->getTitleTags(); 95 | 96 | if(sizeof($titleArray) == 0 || $titleArray->item(0) == NULL) 97 | return; 98 | 99 | //Replace linebreak 100 | $title = $titleArray->item(0)->nodeValue; 101 | $title = str_replace("\n", "", $title); 102 | 103 | //Return if no <title> 104 | if($title == "") 105 | return; 106 | 107 | $description = ""; 108 | $keywords = ""; 109 | 110 | $metasArray = $parser->getMetatags(); 111 | 112 | foreach($metasArray as $meta) 113 | { 114 | if($meta->getAttribute("name") == "description") 115 | $description = $meta->getAttribute("content"); 116 | 117 | if($meta->getAttribute("name") == "keywords") 118 | $keywords = $meta->getAttribute("content"); 119 | } 120 | 121 | $description = str_replace("\n", "", $description); 122 | $keywords = str_replace("\n", "", $keywords); 123 | 124 | //Non-ASCII char encoding 125 | // $title = json_encode($title); 126 | // $description = json_encode($description); 127 | // $keywords = json_encode($keywords); 128 | 129 | if(linkExists($url)) 130 | echo "$url already exists<br>"; 131 | else if(insertLink($url, $title, $description, $keywords)) 132 | echo "SUCCESS: $url<br>"; 133 | else 134 | echo "ERROR: Failed to insert $url<br>"; 135 | 136 | $imageArray = $parser->getImages(); 137 | foreach($imageArray as $image) 138 | { 139 | $src = $image->getAttribute("src"); 140 | $alt = $image->getAttribute("alt"); 141 | $title = $image->getAttribute("title"); 142 | 143 | if(!$title && !$alt) 144 | continue; 145 | 146 | $src = createLink($src, $url); 147 | 148 | if(!in_array($src, $alreadyFoundImages)) 149 | { 150 | $alreadyFoundImages[] = $src; 151 | 152 | if(imageExists($src)) 153 | echo "$src already exists<br>"; 154 | else if(insertImage($url, $src, $alt, $title)) 155 | echo "SUCCESS: $src<br>"; 156 | else 157 | echo "ERROR: Failed to insert $src<br>"; 158 | } 159 | 160 | } 161 | 162 | echo "<b>URL:</b> $url, <b>Title:</b> $title, <b>Description:</b> $description, <b>keywords:</b> $keywords<br>"; //DEBUGGING sites 163 | echo "<b>src:</b> <a href=$src>$src</a>, <b>alt:</b> $alt, <b>title:</b> $title, <b>url:</b> $url<br>"; //DEBUGGING images 164 | } 165 | 166 | function followLinks($url) 167 | { 168 | global $alreadyCrawled; 169 | global $crawling; 170 | 171 | $parser = new DomDocumentParser($url); 172 | 173 | $linkList = $parser->getLinks(); 174 | 175 | 176 | foreach($linkList as $link) 177 | { 178 | $href = $link->getAttribute("href"); 179 | 180 | // Filter hrefs 181 | if(strpos($href, "#") !== false) 182 | continue; 183 | else if(substr($href, 0, 11) == "javascript:") 184 | continue; 185 | 186 | $href = createLink($href, $url); 187 | 188 | if(!in_array($href, $alreadyCrawled)) 189 | { 190 | $alreadyCrawled[] = $href; 191 | $crawling[] = $href; 192 | 193 | getDetails($href); 194 | } 195 | //else return; //DEBUGGING 196 | 197 | echo ($href . "<br>"); //DEBUGGING 198 | } 199 | 200 | array_shift($crawling); 201 | 202 | foreach($crawling as $site) 203 | followLinks($site); 204 | } 205 | 206 | 207 | } 208 | ?> -------------------------------------------------------------------------------- /crawl.php: -------------------------------------------------------------------------------- 1 | <?php 2 | include("config.php"); 3 | include("classes/Crawler.php"); 4 | include("classes/DomDocumentParser.php"); 5 | 6 | if(isset($_SESSION['loggedin'])) 7 | { 8 | exit("You must be logged in!"); 9 | header("location: login.php"); 10 | } 11 | 12 | $alreadyCrawled = array(); 13 | $crawling = array(); 14 | $alreadyFoundImages = array(); 15 | 16 | 17 | function linkExists($url) 18 | { 19 | global $con; 20 | 21 | $query = $con->prepare("SELECT * FROM sites WHERE url = :url"); 22 | 23 | $query->bindParam(":url", $url); 24 | $query->execute(); 25 | 26 | return $query->rowCount() != 0; 27 | } 28 | 29 | function imageExists($src) 30 | { 31 | global $con; 32 | 33 | $query = $con->prepare("SELECT * FROM images WHERE imageUrl = :src"); 34 | 35 | $query->bindParam(":src", $src); 36 | $query->execute(); 37 | 38 | return $query->rowCount() != 0; 39 | } 40 | 41 | 42 | function insertLink($url, $title, $description, $keywords) 43 | { 44 | global $con; 45 | 46 | $query = $con->prepare("INSERT INTO sites(url, title, description, keywords) 47 | VALUES(:url, :title, :description, :keywords)"); 48 | 49 | $query->bindParam(":url", $url); 50 | $query->bindParam(":title", $title); 51 | $query->bindParam(":description", $description); 52 | $query->bindParam(":keywords", $keywords); 53 | 54 | return $query->execute(); 55 | } 56 | 57 | function insertImage($url, $src, $alt, $title) 58 | { 59 | global $con; 60 | 61 | $query = $con->prepare("INSERT INTO images(siteUrl, imageUrl, alt, title) 62 | VALUES(:siteUrl, :imageUrl, :alt, :title)"); 63 | 64 | $query->bindParam(":siteUrl", $url); 65 | $query->bindParam(":imageUrl", $src); 66 | $query->bindParam(":alt", $alt); 67 | $query->bindParam(":title", $title); 68 | 69 | return $query->execute(); 70 | } 71 | 72 | /* Converts relative link to absolute link */ 73 | function createLink($src, $url) 74 | { 75 | $scheme = parse_url($url)["scheme"]; // http 76 | $host = parse_url($url)["host"]; // www.safesploit.com 77 | 78 | if(substr($src, 0, 2) == "//") 79 | $src = $scheme . ":" . $src; 80 | else if(substr($src, 0, 1) == "/") 81 | $src = $scheme . "://" . $host . $src; 82 | else if(substr($src, 0, 2) == "./") 83 | $src = $scheme . "://" . $host . dirname(parse_url($url)["path"]) . substr($src, 1); 84 | else if(substr($src, 0, 3) == "../") 85 | $src = $scheme . "://" . $host . "/" . $src; 86 | else if(substr($src, 0, 5) != "https" && substr($src, 0, 4) != "http") 87 | $src = $scheme . "://" . $host . "/" . $src; 88 | 89 | return $src; 90 | } 91 | 92 | function getDetails($url) 93 | { 94 | global $alreadyFoundImages; 95 | 96 | $parser = new DomDocumentParser($url); 97 | 98 | $titleArray = $parser->getTitleTags(); 99 | 100 | if(sizeof($titleArray) == 0 || $titleArray->item(0) == NULL) 101 | return; 102 | 103 | //Replace linebreak 104 | $title = $titleArray->item(0)->nodeValue; 105 | $title = str_replace("\n", "", $title); 106 | 107 | //Return if no <title> 108 | if($title == "") 109 | return; 110 | 111 | $description = ""; 112 | $keywords = ""; 113 | 114 | $metasArray = $parser->getMetatags(); 115 | 116 | foreach($metasArray as $meta) 117 | { 118 | if($meta->getAttribute("name") == "description") 119 | $description = $meta->getAttribute("content"); 120 | 121 | if($meta->getAttribute("name") == "keywords") 122 | $keywords = $meta->getAttribute("content"); 123 | } 124 | 125 | $description = str_replace("\n", "", $description); 126 | $keywords = str_replace("\n", "", $keywords); 127 | 128 | if(linkExists($url)) 129 | echo "$url already exists<br>"; 130 | else if(insertLink($url, $title, $description, $keywords)) 131 | echo "SUCCESS: $url<br>"; 132 | else 133 | echo "ERROR: Failed to insert $url<br>"; 134 | 135 | $imageArray = $parser->getImages(); 136 | foreach($imageArray as $image) 137 | { 138 | $src = $image->getAttribute("src"); 139 | $alt = $image->getAttribute("alt"); 140 | $title = $image->getAttribute("title"); 141 | 142 | if(!$title && !$alt) 143 | continue; 144 | 145 | $src = createLink($src, $url); 146 | 147 | if(!in_array($src, $alreadyFoundImages)) 148 | { 149 | $alreadyFoundImages[] = $src; 150 | 151 | if(imageExists($src)) 152 | echo "$src already exists<br>"; 153 | else if(insertImage($url, $src, $alt, $title)) 154 | echo "SUCCESS: $src<br>"; 155 | else 156 | echo "ERROR: Failed to insert $src<br>"; 157 | } 158 | 159 | } 160 | 161 | echo "<b>URL:</b> $url, <b>Title:</b> $title, <b>Description:</b> $description, <b>keywords:</b> $keywords<br>"; //DEBUGGING sites 162 | echo "<b>src:</b> <a href=$src>$src</a>, <b>alt:</b> $alt, <b>title:</b> $title, <b>url:</b> $url<br>"; //DEBUGGING images 163 | } 164 | 165 | function followLinks($url) 166 | { 167 | global $alreadyCrawled; 168 | global $crawling; 169 | 170 | $parser = new DomDocumentParser($url); 171 | 172 | $linkList = $parser->getLinks(); 173 | 174 | 175 | foreach($linkList as $link) 176 | { 177 | $href = $link->getAttribute("href"); 178 | 179 | // Filter hrefs 180 | if(strpos($href, "#") !== false) 181 | continue; 182 | else if(substr($href, 0, 11) == "javascript:") 183 | continue; 184 | 185 | $href = createLink($href, $url); 186 | 187 | if(!in_array($href, $alreadyCrawled)) 188 | { 189 | $alreadyCrawled[] = $href; 190 | $crawling[] = $href; 191 | 192 | getDetails($href); 193 | } 194 | //else return; //DEBUGGING 195 | 196 | echo ($href . "<br>"); //DEBUGGING 197 | } 198 | 199 | array_shift($crawling); 200 | 201 | foreach($crawling as $site) 202 | followLinks($site); 203 | } 204 | ?> 205 | 206 | <!DOCTYPE html> 207 | <html> 208 | <head> 209 | <title>doogleBot Crawler 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |
226 |
227 | 228 | Homepage 229 | 230 |
231 |
232 |
233 | URL: 234 | 235 |
236 |
237 |
238 | 239 | 240 | 241 | followLinks($startUrl); 247 | followLinks($startUrl); 248 | } 249 | ?> 250 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | /* Variables */ 4 | --searchbar-border-radius: 20px; 5 | 6 | /* Custom Scrollbar - WebKit */ 7 | --scrollbarWidthSlim: 6px; 8 | --scrollbarBgColourGradient: linear-gradient(180deg, #d0368a 0%, #708ad4 99%); 9 | } 10 | 11 | * 12 | { 13 | font-family: Arial, sans-serif; 14 | color: #545454; 15 | } 16 | 17 | html, 18 | body 19 | { 20 | margin: 0; 21 | height: 100%; 22 | } 23 | 24 | .wrapper 25 | { 26 | display: flex; 27 | flex-direction: column; 28 | min-height: 100%; 29 | } 30 | 31 | .wrapper.indexPage 32 | { 33 | justify-content: center; 34 | } 35 | 36 | .mainSection 37 | { 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | } 42 | 43 | .mainSection .searchContainer 44 | { 45 | margin-top: 20px; 46 | width: 100%; 47 | } 48 | 49 | .mainSection .searchContainer form 50 | { 51 | display: flex; 52 | flex-direction: column; 53 | align-items: center; 54 | } 55 | 56 | .searchContainer .searchButton 57 | { 58 | color: #757575; 59 | background-color: #f5f5f5; 60 | border: none; 61 | height: 36px; 62 | width: 125px; 63 | border-radius: 2px; 64 | font-size: 13px; 65 | font-weight: bold; 66 | margin-top: 20px; 67 | cursor: pointer; 68 | outline: none; 69 | border-radius: 25px; /* Bug fix */ 70 | } 71 | 72 | .mainSection .searchContainer .searchBox 73 | { 74 | border: none; 75 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 76 | height: 44px; 77 | border-radius: var(--searchbar-border-radius); 78 | outline: none; 79 | padding: 10px; 80 | box-sizing: border-box; 81 | font-size: 16px; 82 | width: 70%; 83 | max-width: 630px; 84 | color: #000; 85 | padding-left: 25px; 86 | } 87 | 88 | /* .mainSection .searchContainer .searchBox:hover 89 | { 90 | border: aqua; 91 | } */ 92 | 93 | .mainSection .logoContainer 94 | { 95 | width: 220px; 96 | text-align: center; 97 | } 98 | 99 | .logoContainer img 100 | { 101 | width: 100%; 102 | } 103 | 104 | /****************** 105 | Search Page Styling 106 | ******************/ 107 | .header 108 | { 109 | background-color: #FAFAFA; 110 | border-bottom: 1px solid #ebebeb; 111 | } 112 | 113 | .wrapper .headerContent 114 | { 115 | display: flex; 116 | align-items: center; 117 | } 118 | 119 | .headerContent .logoContainer 120 | { 121 | width: 150px; 122 | padding: 5px 20px; 123 | box-sizing: border-box; 124 | } 125 | 126 | /* Search container */ 127 | .headerContent .searchContainer 128 | { 129 | flex: 1; 130 | } 131 | 132 | .headerContent .searchContainer form 133 | { 134 | margin: 15px 0 28px 0; 135 | } 136 | 137 | .headerContent .searchBarContainer 138 | { 139 | height: 44px; 140 | background-color: #fff; 141 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 142 | width: 70%; 143 | max-width: 630px; 144 | box-sizing: border-box; 145 | display: flex; 146 | border-radius: var(--searchbar-border-radius); 147 | } 148 | 149 | .headerContent .searchBarContainer .searchBox 150 | { 151 | flex: 1; 152 | border: none; 153 | background-color: transparent; 154 | padding: 12px; 155 | font-size: 16px; 156 | color: #000; 157 | outline: none; 158 | } 159 | 160 | .headerContent .searchBarContainer .searchButton 161 | { 162 | background-color: #fff; 163 | height: 44px; 164 | margin-top: 0; 165 | width: 44px; 166 | padding-right: 20px; 167 | display: flex; 168 | justify-content: center; 169 | } 170 | 171 | .headerContent .searchBarContainer .searchButton img 172 | { 173 | width: 22px; 174 | padding: 10px; 175 | } 176 | 177 | /* Tabs container */ 178 | .tabsContainer 179 | { 180 | margin-left: 150px; 181 | } 182 | 183 | .tabsContainer .tabList 184 | { 185 | padding: 0; 186 | margin: 0; 187 | } 188 | 189 | .tabsContainer .tabList li 190 | { 191 | display: inline-block; 192 | padding: 0 16px 12px 16px; 193 | color: #777; 194 | font-size: 13px; 195 | } 196 | 197 | .tabsContainer .tabList li a 198 | { 199 | text-decoration: none; 200 | } 201 | 202 | .tabsContainer .tabList li.active 203 | { 204 | border-bottom: 3px solid #1A73E8; 205 | } 206 | 207 | .tabsContainer .tabList li.active a 208 | { 209 | font-weight: bold; 210 | color: #1A73E8; 211 | } 212 | 213 | /**************** 214 | Results styling 215 | ****************/ 216 | .mainResultsSection 217 | { 218 | flex: 1; 219 | } 220 | 221 | .mainResultsSection .resultsCount 222 | { 223 | font-size: 13px; 224 | color: #808080; 225 | margin-left: 150px; 226 | } 227 | 228 | .mainResultsSection .siteResults 229 | { 230 | margin-left: 150px; 231 | } 232 | 233 | .resultContainer 234 | { 235 | display: flex; 236 | flex-direction: column; 237 | margin-bottom: 26px; 238 | } 239 | 240 | .resultContainer .title 241 | { 242 | margin: 0; 243 | } 244 | 245 | .resultContainer .title a 246 | { 247 | color: #1a0dab; 248 | text-decoration: none; 249 | font-weight: normal; 250 | font-size: 18px; 251 | } 252 | 253 | .resultContainer .title a:hover 254 | { 255 | text-decoration: underline; 256 | } 257 | 258 | .resultContainer .url 259 | { 260 | color: #006621; 261 | font-size: 14px; 262 | } 263 | 264 | .resultContainer .description 265 | { 266 | font-size: 12px; 267 | } 268 | 269 | /******************** 270 | Pagination styling 271 | ********************/ 272 | .paginationContainer 273 | { 274 | display: flex; 275 | justify-content: center; 276 | margin-bottom: 25px; 277 | } 278 | 279 | .pageButtons 280 | { 281 | display: flex; 282 | } 283 | 284 | .pageNumberContainer img 285 | { 286 | height: 37px; 287 | } 288 | 289 | #pageEndContainer img 290 | { 291 | height: 43px; 292 | } 293 | 294 | .pageNumberContainer, 295 | .pageNumberContainer a 296 | { 297 | display: flex; 298 | flex-direction: column; 299 | align-items: center; 300 | text-decoration: none; 301 | } 302 | 303 | .pageNumber 304 | { 305 | color: #000; 306 | font-size: 13px; 307 | } 308 | 309 | a .pageNumber 310 | { 311 | color: #4285f4; 312 | } 313 | 314 | /************** 315 | Image styling 316 | **************/ 317 | .mainResultsSection .imageResults 318 | { 319 | margin: 20px; 320 | } 321 | 322 | .gridItem 323 | { 324 | position: relative; 325 | } 326 | 327 | .gridItem img 328 | { 329 | max-width: 200px; 330 | min-width: 50px; 331 | visibility: hidden; 332 | } 333 | 334 | .gridItem .details 335 | { 336 | visibility: hidden; 337 | position: absolute; 338 | bottom: 0px; 339 | left: 0px; 340 | width: 100%; 341 | overflow: hidden; 342 | background-color: rgba(0,0,0,0.8); 343 | color: #fff; 344 | font-size: 11px; 345 | padding: 3px; 346 | box-sizing: border-box; 347 | white-space: nowrap; 348 | } 349 | 350 | .gridItem:hover .details 351 | { 352 | visibility: visible; 353 | } 354 | 355 | /********* 356 | Crawl form 357 | **********/ 358 | #crawl-wrapper 359 | { 360 | text-align: center; 361 | padding-top: 100px; 362 | } 363 | 364 | #crawl-input 365 | { 366 | width:400px; 367 | } 368 | 369 | 370 | /* 371 | Mobile responsive design -- Overrides 372 | */ 373 | @media only screen and (max-width: 700px) 374 | { 375 | .resultContainer .url 376 | { 377 | color: #006621; 378 | font-size: 19px; 379 | } 380 | 381 | .tabsContainer 382 | { 383 | text-align: center; 384 | } 385 | 386 | .mainResultsSection .resultsCount 387 | { 388 | margin-left: 15px; 389 | } 390 | 391 | .mainResultsSection .siteResults 392 | { 393 | margin-left: 15px; 394 | } 395 | 396 | .mainResultsSection .resultsCount 397 | { 398 | margin-left: 15px; 399 | } 400 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doogle 2 | Doogle is a search engine and web crawler which can search indexed websites and images, and then use keywords to be searched later. 3 | 4 | Written primarily in OOP style PHP with the intent of better understanding OOP and how web crawlers work. 5 | 6 |

7 | DoogleHomepage-Preview 8 |

9 | 10 | # Features 11 | 12 | - Search sites 13 | * Displays title, URL and description 14 | - Search images 15 | * Hover over images to preview description (alt tag) 16 | * Masonry layout for searched images 17 | * Image preview using Fancybox 18 | * Image search page responds dynamically 19 | - Clean homepage 20 | - Filters broken image results 21 | - Organises search results by clicks/visits 22 | - Pagination system at the bottom of the search page 23 | - Shows 'results found' for search term 24 | - Supports non-latin characters (UTF-8) 25 | 26 | # Table of Contents 27 | 28 | - [Setup and Usage](#setup-and-usage) 29 | - [Docker](#docker) 30 | - [Server Setup](#server-setup) 31 | - [PHP Dependencies](#php-dependencies) 32 | - [Connecting PHP to MySQL Server](#connecting-php-to-mysql-server) 33 | - [Crawling Websites to Populate Images and Sites tables](#crawling-websites-to-populate-images-and-sites-tables) 34 | - [Programming Logic](#programming-logic) 35 | - [Pagination](#pagination) 36 | - [Image Search](#image-search) 37 | - [Site Search - Trimming Results](#site-search---trimming-results) 38 | - [Telemetry](#telemetry) 39 | - [User-Agent](#user-agent) 40 | - [Preview Images](#preview-images) 41 | - [Doogle Homepage](#doogle-homepage) 42 | - [Doogle Search - Sites](#doogle-search---sites) 43 | - [Doogle Search - Images](#doogle-search---images) 44 | - [Pagination System](#pagination-system) 45 | - [doogleBot Crawl Form](#dooglebot-crawl-form) 46 | - [Preview Video](#preview-video) 47 | 48 | # Setup and Usage 49 | 50 | Two methods of setup are discussed. 51 | - Docker (Easiest) 52 | - Server Setup 53 | 54 | ## Docker 55 | 56 | Docker configuration files are available at [doogle-docker](https://github.com/safesploit/doogle-docker). 57 | 58 | Presuming you already have [Docker](https://www.docker.com/) v3.9 (or greater) installed and configured. 59 | 60 | git clone https://github.com/safesploit/doogle-docker.git 61 | cd doogle-docker 62 | sh build.sh 63 | 64 |

65 | Screenshot 2023-02-22 at 21 11 33 66 | image 67 | 68 | Doogle is now accessible via [localhost:8000](http://localhost:8000). 69 | 70 | For debugging phpMyAdmin has also been included on [localhost:8001](http://localhost:8001). 71 | 72 |

73 | 74 | ## Server Setup 75 | 76 | v1.0.0-beta.1 is supported and tested in PHP 7.4, 8.0 and 8.1. 77 | 78 | Please refer to [XAMPP](https://www.apachefriends.org/index.html) for the web server, PHP server and MySQL server configuration. 79 | XAMPP is the simplest method as several servers are required to use Doogle. 80 | 81 | [MySQL Setup on XAMPP](https://www.rose-hulman.edu/class/se/csse290-WebProgramming/201520/SupportCode/SQL-setup.html) will use PHPMyAdmin as a GUI method of setting up the database. 82 | 83 | Once logged into the database via PHPMyAdmin under the **PHPMyAdmin > SQL** tab, the content of 'doogle-tables-no-data.sql' can be pasted into the field 84 | 85 | Image1-PHPMyAdmin 86 | 87 | ## PHP Dependencies 88 | 89 | mysql 90 | pdo_mysql 91 | 92 | 93 | ### SQL User Creation 94 | 95 | Amend the password _PASSWORD_HERE_ using a strong [random password](https://passwordsgenerator.net/). 96 | 97 | mysql> CREATE USER IF NOT EXISTS 'doogle'@'localhost' IDENTIFIED BY 'PASSWORD_HERE'; 98 | 99 | ### SQL User Permissions 100 | 101 | The SQL user 'doogle' must have SELECT, INSERT and UPDATE privileges: 102 | 103 | mysql> GRANT SELECT, INSERT, UPDATE ON `doogle`.* TO 'doogle'@'localhost'; 104 | 105 | - INSERT is used for crawling 106 | - SELECT is required for the search engine to return queries 107 | - UPDATE is required to amend the clicks and broken results (see ./ajax/) 108 | 109 | ## Connecting PHP to MySQL Server 110 | 111 | In the file config.php the following must be entered correctly for your database configuration: 112 | 113 | $dbname = "doogle"; 114 | $dbhost = "localhost"; 115 | $dbuser = "doogle"; 116 | $dbpass = ""; 117 | 118 | In the file 'doogle-tables-no-data.sql' the database will be created as 'doogle'. 119 | 120 | ## Crawling Websites to Populate Images and Sites tables 121 | 122 | ### Form-based crawl 123 | 124 | In your browser go to where the file is hosted http://localhost/crawl.php 125 | 126 | Paste the URL into the input field and press the Crawl button. 127 | 128 | ### Manual crawl 129 | 130 | At the bottom of crawl-manual.php the variable $startUrl is where to paste the URL of the website to be crawled: 131 | 132 | $startUrl = "https://thehackernews.com/"; 133 | 134 | Then in your browser go to where the file is hosted http://localhost/crawl-manual.php 135 | 136 | ### Explanation 137 | 138 | The crawling process will take some time, it will completely depend on the size of the website being crawled. 139 | The page will continue to load (without output) until the `crawl.php` script finishes. 140 | 141 | Check the tables `images` and `sites` in the database to ensure they are being populated. 142 | 143 | Image2-PHPMyAdmin 144 | 145 | 146 | Once the tables are populated visit the Doogle homepage and search! 147 | See preview images. 148 | 149 | # Programming Logic 150 | 151 | ## Pagination 152 | 153 | ### Logic of pagination system 154 | Inside search.php, pagination is implemented 155 | 156 | image demonstrating pagnigation 157 | 158 | In the example above, currentPage=11. 159 | The number of pages to show is always 10. 160 | 161 | ### Results Per Page 162 | 163 | Site search will return 20 results per page and image search will return 30 results per page. 164 | 165 | The results per page can be changed inside search.php on lines {83, 88} respectively. As indicated by the $pageSize variables: 166 | 167 | Search-resultsPerPage 168 | 169 | 170 | ### Handling an edge case 171 | 172 | An edge case can occur when no more pages are available. 173 | 174 | So, for 331 results, **17 pages** will be available. However, without an edge case scenario consider, the UI for the pagination system will allow scrolling through pages which don't exist; which would return an empty result. 175 | 176 | To handle an edge case the following logic is implemented in the while-loop: 177 | 178 | if($currentPage + $pagesLeft > $numPages + 1) 179 | $currentPage = $numPages + 1 - $pagesLeft; 180 | 181 | while($pagesLeft != 0 && $currentPage <= $numPages) 182 | { ... } 183 | 184 | 185 | ## Image Search 186 | 187 | ### Image Captions 188 | 189 | To make image searches more informative, the 'alt' tag is part of the search term. As shown in ./classes/ImageResultsProvider.php line 34 190 | 191 | ImageResultsProvider-query 192 | 193 | 194 | ### Loading Images with JavaScript 195 | In the 'images' table, there is a row 'broken' which tracks images which return an error. 196 | 197 | Because images are already loaded with a pure server-side solution, AJAX must be leveraged, loading images dynamically. Which is shown in ./assets/js/script.js 198 | 199 | 200 | script js-loadImage-broken 201 | 202 | 203 | 204 | 205 | ### Masonry 206 | Image searches are using [Masonry - Cascading grid layout library](https://masonry.desandro.com/). 207 | 208 | Masonry allows images a grid layout which is responsive due to jQuery. 209 | The image below shows an example layout: 210 | 211 | Masonry-item-layout 212 | 213 | 214 | 215 | ## Site Search - Trimming Results 216 | 217 | As shown in the preview images, Doogle when performing a site search will return (title, URL and description) for each result. 218 | 219 | However, to make some results easier to read, a trimming process is performed. Inside ./classes/SiteResultsProvider.php the function trimField() is called: 220 | 221 | SiteResultsProvider-trim1 222 | 223 | SiteResultsProvider-trim2 224 | 225 | Title's are trimmed at 55 characters and description's are trimmed at 230 characters. 226 | 227 | 228 | ## Telemetry 229 | 230 | Both the 'images' and 'sites' tables in the database have a row containing 'clicks' for each column. 231 | 232 | The 'clicks' field is increased each time a site is visited or image is previewed. 233 | 234 | When performing a search, results returned are organised in descending order of clicks. 235 | This behaviour is shown by the $query inside ./classes/SiteResultsProvider.php function getResultsHtml(). See line 43. 236 | 237 | SiteResultsProvider-getResultsHtml 238 | 239 | 240 | ## User-Agent 241 | 242 | Inside ./classes/DomDocumentParser.php the user-agent data used during crawling is located. 243 | As indicated on line 9: 244 | 245 | DomDocumentParser-bot 246 | 247 | 248 | # Preview Images 249 | ## Doogle Homepage 250 | 251 | Image3-DoogleHomepage-Edge 252 | 253 | ## Doogle Search - Sites 254 | 255 | Image4-DoogleSearch-PoC 256 | 257 | ## Doogle Search - Images 258 | 259 | Image5-DoogleSearch-PoC-images 260 | 261 | ### Image Preview 262 | 263 | Image preview is done using Fancybox. 264 | 265 | The title, image URL and site URL are available on the bottom left corner. 266 | 267 | Image9-DoogleSearch-imagePreview 268 | 269 | 270 | 271 | ## Pagination System 272 | 273 | Naturally, certain search terms may return many results like 'bbc'. 274 | 275 | To which Doogle only displays **20 sites** per page. 276 | At the bottom of the page, we can view the next 10 pages. 277 | 278 | ### Results Shown 279 | 280 | Image6-DoogleSearch-pagination-ResultsShown 281 | 282 | ### Bottom of Page 283 | 284 | Image7-DoogleSearch-pagination-Bottom 285 | 286 | ### Bottom of Page 13 287 | 288 | Image8-DoogleSearch-pagination-scrollingThrough 289 | 290 | ## doogleBot Crawl Form 291 | 292 | An HTML form to submit a URL for crawling 293 | 294 | Image10-doogleBot-Crawler-formpng 295 | 296 | # Preview Video 297 | 298 | [Doogle Search demo - YouTube](https://youtu.be/clDt4Sg7ako) 299 | -------------------------------------------------------------------------------- /assets/css/fancybox/3.3.5/jquery.fancybox.min.css: -------------------------------------------------------------------------------- 1 | body.compensate-for-scrollbar{overflow:hidden}.fancybox-active{height:auto}.fancybox-is-hidden{left:-9999px;margin:0;position:absolute!important;top:-9999px;visibility:hidden}.fancybox-container{-webkit-backface-visibility:hidden;backface-visibility:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;height:100%;left:0;position:fixed;-webkit-tap-highlight-color:transparent;top:0;-webkit-transform:translateZ(0);transform:translateZ(0);width:100%;z-index:99992}.fancybox-container *{box-sizing:border-box}.fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-stage{bottom:0;left:0;position:absolute;right:0;top:0}.fancybox-outer{-webkit-overflow-scrolling:touch;overflow-y:auto}.fancybox-bg{background:#1e1e1e;opacity:0;transition-duration:inherit;transition-property:opacity;transition-timing-function:cubic-bezier(.47,0,.74,.71)}.fancybox-is-open .fancybox-bg{opacity:.87;transition-timing-function:cubic-bezier(.22,.61,.36,1)}.fancybox-caption,.fancybox-infobar,.fancybox-navigation .fancybox-button,.fancybox-toolbar{direction:ltr;opacity:0;position:absolute;transition:opacity .25s,visibility 0s linear .25s;visibility:hidden;z-index:99997}.fancybox-show-caption .fancybox-caption,.fancybox-show-infobar .fancybox-infobar,.fancybox-show-nav .fancybox-navigation .fancybox-button,.fancybox-show-toolbar .fancybox-toolbar{opacity:1;transition:opacity .25s,visibility 0s;visibility:visible}.fancybox-infobar{color:#ccc;font-size:13px;-webkit-font-smoothing:subpixel-antialiased;height:44px;left:0;line-height:44px;min-width:44px;mix-blend-mode:difference;padding:0 10px;pointer-events:none;text-align:center;top:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fancybox-toolbar{right:0;top:0}.fancybox-stage{direction:ltr;overflow:visible;-webkit-transform:translateZ(0);z-index:99994}.fancybox-is-open .fancybox-stage{overflow:hidden}.fancybox-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:none;height:100%;left:0;outline:none;overflow:auto;-webkit-overflow-scrolling:touch;padding:44px;position:absolute;text-align:center;top:0;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;white-space:normal;width:100%;z-index:99994}.fancybox-slide:before{content:"";display:inline-block;height:100%;margin-right:-.25em;vertical-align:middle;width:0}.fancybox-is-sliding .fancybox-slide,.fancybox-slide--current,.fancybox-slide--next,.fancybox-slide--previous{display:block}.fancybox-slide--next{z-index:99995}.fancybox-slide--image{overflow:visible;padding:44px 0}.fancybox-slide--image:before{display:none}.fancybox-slide--html{padding:6px 6px 0}.fancybox-slide--iframe{padding:44px 44px 0}.fancybox-content{background:#fff;display:inline-block;margin:0 0 6px;max-width:100%;overflow:auto;padding:0;padding:24px;position:relative;text-align:left;vertical-align:middle}.fancybox-slide--image .fancybox-content{-webkit-animation-timing-function:cubic-bezier(.5,0,.14,1);animation-timing-function:cubic-bezier(.5,0,.14,1);-webkit-backface-visibility:hidden;backface-visibility:hidden;background:transparent;background-repeat:no-repeat;background-size:100% 100%;left:0;margin:0;max-width:none;overflow:visible;padding:0;position:absolute;top:0;-webkit-transform-origin:top left;transform-origin:top left;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:99995}.fancybox-can-zoomOut .fancybox-content{cursor:zoom-out}.fancybox-can-zoomIn .fancybox-content{cursor:zoom-in}.fancybox-can-drag .fancybox-content{cursor:-webkit-grab;cursor:grab}.fancybox-is-dragging .fancybox-content{cursor:-webkit-grabbing;cursor:grabbing}.fancybox-container [data-selectable=true]{cursor:text}.fancybox-image,.fancybox-spaceball{background:transparent;border:0;height:100%;left:0;margin:0;max-height:none;max-width:none;padding:0;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:100%}.fancybox-spaceball{z-index:1}.fancybox-slide--html .fancybox-content{margin-bottom:6px}.fancybox-slide--iframe .fancybox-content,.fancybox-slide--map .fancybox-content,.fancybox-slide--video .fancybox-content{height:100%;margin:0;overflow:visible;padding:0;width:100%}.fancybox-slide--video .fancybox-content{background:#000}.fancybox-slide--map .fancybox-content{background:#e5e3df}.fancybox-slide--iframe .fancybox-content{background:#fff;height:calc(100% - 44px);margin-bottom:44px}.fancybox-iframe,.fancybox-video{background:transparent;border:0;height:100%;margin:0;overflow:hidden;padding:0;width:100%}.fancybox-iframe{vertical-align:top}.fancybox-error{background:#fff;cursor:default;max-width:400px;padding:40px;width:100%}.fancybox-error p{color:#444;font-size:16px;line-height:20px;margin:0;padding:0}.fancybox-button{background:rgba(30,30,30,.6);border:0;border-radius:0;cursor:pointer;display:inline-block;height:44px;margin:0;outline:none;padding:10px;transition:color .2s;vertical-align:top;width:44px}.fancybox-button,.fancybox-button:link,.fancybox-button:visited{color:#ccc}.fancybox-button:focus,.fancybox-button:hover{color:#fff}.fancybox-button.disabled,.fancybox-button.disabled:hover,.fancybox-button[disabled],.fancybox-button[disabled]:hover{color:#888;cursor:default}.fancybox-button svg{display:block;overflow:visible;position:relative;shape-rendering:geometricPrecision}.fancybox-button svg path{fill:transparent;stroke:currentColor;stroke-linejoin:round;stroke-width:3}.fancybox-button--pause svg path:nth-child(1),.fancybox-button--play svg path:nth-child(2){display:none}.fancybox-button--play svg path,.fancybox-button--share svg path,.fancybox-button--thumbs svg path{fill:currentColor}.fancybox-button--share svg path{stroke-width:1}.fancybox-navigation .fancybox-button{height:38px;opacity:0;padding:6px;position:absolute;top:50%;width:38px}.fancybox-show-nav .fancybox-navigation .fancybox-button{transition:opacity .25s,visibility 0s,color .25s}.fancybox-navigation .fancybox-button:after{content:"";left:-25px;padding:50px;position:absolute;top:-25px}.fancybox-navigation .fancybox-button--arrow_left{left:6px}.fancybox-navigation .fancybox-button--arrow_right{right:6px}.fancybox-close-small{background:transparent;border:0;border-radius:0;color:#555;cursor:pointer;height:44px;margin:0;padding:6px;position:absolute;right:0;top:0;width:44px;z-index:10}.fancybox-close-small svg{fill:transparent;opacity:.8;stroke:currentColor;stroke-width:1.5;transition:stroke .1s}.fancybox-close-small:focus{outline:none}.fancybox-close-small:hover svg{opacity:1}.fancybox-slide--iframe .fancybox-close-small,.fancybox-slide--image .fancybox-close-small,.fancybox-slide--video .fancybox-close-small{color:#ccc;padding:5px;right:-12px;top:-44px}.fancybox-slide--iframe .fancybox-close-small:hover svg,.fancybox-slide--image .fancybox-close-small:hover svg,.fancybox-slide--video .fancybox-close-small:hover svg{background:transparent;color:#fff}.fancybox-is-scaling .fancybox-close-small,.fancybox-is-zoomable.fancybox-can-drag .fancybox-close-small{display:none}.fancybox-caption{bottom:0;color:#fff;font-size:14px;font-weight:400;left:0;line-height:1.5;padding:25px 44px;right:0}.fancybox-caption:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAEtCAQAAABjBcL7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUKM+Vk8EOgDAIQ0vj/3+xBw8qIZZueFnIKC90MCAI8DlrkHGeqqGIU6lVigrBtpCWqeRWoHDNqs0F7VNVBVxmHRlvoVqjaYkdnDIaivH2HqZ5+oZj3JUzWB+cOz4G48Bg+tsJ/tqu4dLC/4Xb+0GcF5BwBC0AA53qAAAAAElFTkSuQmCC);background-repeat:repeat-x;background-size:contain;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:-25px;z-index:-1}.fancybox-caption:after{border-bottom:1px solid hsla(0,0%,100%,.3);content:"";display:block;left:44px;position:absolute;right:44px;top:0}.fancybox-caption a,.fancybox-caption a:link,.fancybox-caption a:visited{color:#ccc;text-decoration:none}.fancybox-caption a:hover{color:#fff;text-decoration:underline}.fancybox-loading{-webkit-animation:a .8s infinite linear;animation:a .8s infinite linear;background:transparent;border:6px solid hsla(0,0%,39%,.5);border-radius:100%;border-top-color:#fff;height:60px;left:50%;margin:-30px 0 0 -30px;opacity:.6;padding:0;position:absolute;top:50%;width:60px;z-index:99999}@-webkit-keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fancybox-animated{transition-timing-function:cubic-bezier(0,0,.25,1)}.fancybox-fx-slide.fancybox-slide--previous{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.fancybox-fx-slide.fancybox-slide--next{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.fancybox-fx-slide.fancybox-slide--current{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}.fancybox-fx-fade.fancybox-slide--next,.fancybox-fx-fade.fancybox-slide--previous{opacity:0;transition-timing-function:cubic-bezier(.19,1,.22,1)}.fancybox-fx-fade.fancybox-slide--current{opacity:1}.fancybox-fx-zoom-in-out.fancybox-slide--previous{opacity:0;-webkit-transform:scale3d(1.5,1.5,1.5);transform:scale3d(1.5,1.5,1.5)}.fancybox-fx-zoom-in-out.fancybox-slide--next{opacity:0;-webkit-transform:scale3d(.5,.5,.5);transform:scale3d(.5,.5,.5)}.fancybox-fx-zoom-in-out.fancybox-slide--current{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}.fancybox-fx-rotate.fancybox-slide--previous{opacity:0;-webkit-transform:rotate(-1turn);transform:rotate(-1turn)}.fancybox-fx-rotate.fancybox-slide--next{opacity:0;-webkit-transform:rotate(1turn);transform:rotate(1turn)}.fancybox-fx-rotate.fancybox-slide--current{opacity:1;-webkit-transform:rotate(0deg);transform:rotate(0deg)}.fancybox-fx-circular.fancybox-slide--previous{opacity:0;-webkit-transform:scale3d(0,0,0) translate3d(-100%,0,0);transform:scale3d(0,0,0) translate3d(-100%,0,0)}.fancybox-fx-circular.fancybox-slide--next{opacity:0;-webkit-transform:scale3d(0,0,0) translate3d(100%,0,0);transform:scale3d(0,0,0) translate3d(100%,0,0)}.fancybox-fx-circular.fancybox-slide--current{opacity:1;-webkit-transform:scaleX(1) translateZ(0);transform:scaleX(1) translateZ(0)}.fancybox-fx-tube.fancybox-slide--previous{-webkit-transform:translate3d(-100%,0,0) scale(.1) skew(-10deg);transform:translate3d(-100%,0,0) scale(.1) skew(-10deg)}.fancybox-fx-tube.fancybox-slide--next{-webkit-transform:translate3d(100%,0,0) scale(.1) skew(10deg);transform:translate3d(100%,0,0) scale(.1) skew(10deg)}.fancybox-fx-tube.fancybox-slide--current{-webkit-transform:translateZ(0) scale(1);transform:translateZ(0) scale(1)}.fancybox-share{background:#f4f4f4;border-radius:3px;max-width:90%;padding:30px;text-align:center}.fancybox-share h1{color:#222;font-size:35px;font-weight:700;margin:0 0 20px}.fancybox-share p{margin:0;padding:0}.fancybox-share__button{border:0;border-radius:3px;display:inline-block;font-size:14px;font-weight:700;line-height:40px;margin:0 5px 10px;min-width:130px;padding:0 15px;text-decoration:none;transition:all .2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}.fancybox-share__button:link,.fancybox-share__button:visited{color:#fff}.fancybox-share__button:hover{text-decoration:none}.fancybox-share__button--fb{background:#3b5998}.fancybox-share__button--fb:hover{background:#344e86}.fancybox-share__button--pt{background:#bd081d}.fancybox-share__button--pt:hover{background:#aa0719}.fancybox-share__button--tw{background:#1da1f2}.fancybox-share__button--tw:hover{background:#0d95e8}.fancybox-share__button svg{height:25px;margin-right:7px;position:relative;top:-1px;vertical-align:middle;width:25px}.fancybox-share__button svg path{fill:#fff}.fancybox-share__input{background:transparent;border:0;border-bottom:1px solid #d7d7d7;border-radius:0;color:#5d5b5b;font-size:14px;margin:10px 0 0;outline:none;padding:10px 15px;width:100%}.fancybox-thumbs{background:#fff;bottom:0;display:none;margin:0;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;padding:2px 2px 4px;position:absolute;right:0;-webkit-tap-highlight-color:transparent;top:0;width:212px;z-index:99995}.fancybox-thumbs-x{overflow-x:auto;overflow-y:hidden}.fancybox-show-thumbs .fancybox-thumbs{display:block}.fancybox-show-thumbs .fancybox-inner{right:212px}.fancybox-thumbs>ul{font-size:0;height:100%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0;position:absolute;position:relative;white-space:nowrap;width:100%}.fancybox-thumbs-x>ul{overflow:hidden}.fancybox-thumbs-y>ul::-webkit-scrollbar{width:7px}.fancybox-thumbs-y>ul::-webkit-scrollbar-track{background:#fff;border-radius:10px;box-shadow:inset 0 0 6px rgba(0,0,0,.3)}.fancybox-thumbs-y>ul::-webkit-scrollbar-thumb{background:#2a2a2a;border-radius:10px}.fancybox-thumbs>ul>li{-webkit-backface-visibility:hidden;backface-visibility:hidden;cursor:pointer;float:left;height:75px;margin:2px;max-height:calc(100% - 8px);max-width:calc(50% - 4px);outline:none;overflow:hidden;padding:0;position:relative;-webkit-tap-highlight-color:transparent;width:100px}.fancybox-thumbs-loading{background:rgba(0,0,0,.1)}.fancybox-thumbs>ul>li{background-position:50%;background-repeat:no-repeat;background-size:cover}.fancybox-thumbs>ul>li:before{border:4px solid #4ea7f9;bottom:0;content:"";left:0;opacity:0;position:absolute;right:0;top:0;transition:all .2s cubic-bezier(.25,.46,.45,.94);z-index:99991}.fancybox-thumbs .fancybox-thumbs-active:before{opacity:1}@media (max-width:800px){.fancybox-thumbs{width:110px}.fancybox-show-thumbs .fancybox-inner{right:110px}.fancybox-thumbs>ul>li{max-width:calc(100% - 10px)}} -------------------------------------------------------------------------------- /assets/js/masonry/4.2.2/masonry.pkgd.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Masonry PACKAGED v4.2.2 3 | * Cascading grid layout library 4 | * https://masonry.desandro.com 5 | * MIT License 6 | * by David DeSandro 7 | */ 8 | 9 | !function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,r,a){function h(t,e,n){var o,r="$()."+i+'("'+e+'")';return t.each(function(t,h){var u=a.data(h,i);if(!u)return void s(i+" not initialized. Cannot call methods, i.e. "+r);var d=u[e];if(!d||"_"==e.charAt(0))return void s(r+" is not a valid method");var l=d.apply(u,n);o=void 0===o?l:o}),void 0!==o?o:t}function u(t,e){t.each(function(t,n){var o=a.data(n,i);o?(o.option(e),o._init()):(o=new r(n,e),a.data(n,i,o))})}a=a||e||t.jQuery,a&&(r.prototype.option||(r.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=o.call(arguments,1);return h(this,t,e)}return u(this,t),this},n(a))}function n(t){!t||t&&t.bridget||(t.bridget=i)}var o=Array.prototype.slice,r=t.console,s="undefined"==typeof r?function(){}:function(t){r.error(t)};return n(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;oe;e++){var i=h[e];t[i]=0}return t}function n(t){var e=getComputedStyle(t);return e||a("Style returned "+e+". Are you running this code in a hidden iframe on Firefox? See https://bit.ly/getsizebug1"),e}function o(){if(!d){d=!0;var e=document.createElement("div");e.style.width="200px",e.style.padding="1px 2px 3px 4px",e.style.borderStyle="solid",e.style.borderWidth="1px 2px 3px 4px",e.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(e);var o=n(e);s=200==Math.round(t(o.width)),r.isBoxSizeOuter=s,i.removeChild(e)}}function r(e){if(o(),"string"==typeof e&&(e=document.querySelector(e)),e&&"object"==typeof e&&e.nodeType){var r=n(e);if("none"==r.display)return i();var a={};a.width=e.offsetWidth,a.height=e.offsetHeight;for(var d=a.isBorderBox="border-box"==r.boxSizing,l=0;u>l;l++){var c=h[l],f=r[c],m=parseFloat(f);a[c]=isNaN(m)?0:m}var p=a.paddingLeft+a.paddingRight,g=a.paddingTop+a.paddingBottom,y=a.marginLeft+a.marginRight,v=a.marginTop+a.marginBottom,_=a.borderLeftWidth+a.borderRightWidth,z=a.borderTopWidth+a.borderBottomWidth,E=d&&s,b=t(r.width);b!==!1&&(a.width=b+(E?0:p+_));var x=t(r.height);return x!==!1&&(a.height=x+(E?0:g+z)),a.innerWidth=a.width-(p+_),a.innerHeight=a.height-(g+z),a.outerWidth=a.width+y,a.outerHeight=a.height+v,a}}var s,a="undefined"==typeof console?e:function(t){console.error(t)},h=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],u=h.length,d=!1;return r}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("desandro-matches-selector/matches-selector",e):"object"==typeof module&&module.exports?module.exports=e():t.matchesSelector=e()}(window,function(){"use strict";var t=function(){var t=window.Element.prototype;if(t.matches)return"matches";if(t.matchesSelector)return"matchesSelector";for(var e=["webkit","moz","ms","o"],i=0;is?"round":"floor";r=Math[a](r),this.cols=Math.max(r,1)},n.getContainerWidth=function(){var t=this._getOption("fitWidth"),i=t?this.element.parentNode:this.element,n=e(i);this.containerWidth=n&&n.innerWidth},n._getItemLayoutPosition=function(t){t.getSize();var e=t.size.outerWidth%this.columnWidth,i=e&&1>e?"round":"ceil",n=Math[i](t.size.outerWidth/this.columnWidth);n=Math.min(n,this.cols);for(var o=this.options.horizontalOrder?"_getHorizontalColPosition":"_getTopColPosition",r=this[o](n,t),s={x:this.columnWidth*r.col,y:r.y},a=r.y+t.size.outerHeight,h=n+r.col,u=r.col;h>u;u++)this.colYs[u]=a;return s},n._getTopColPosition=function(t){var e=this._getTopColGroup(t),i=Math.min.apply(Math,e);return{col:e.indexOf(i),y:i}},n._getTopColGroup=function(t){if(2>t)return this.colYs;for(var e=[],i=this.cols+1-t,n=0;i>n;n++)e[n]=this._getColGroupY(n,t);return e},n._getColGroupY=function(t,e){if(2>e)return this.colYs[t];var i=this.colYs.slice(t,t+e);return Math.max.apply(Math,i)},n._getHorizontalColPosition=function(t,e){var i=this.horizontalColIndex%this.cols,n=t>1&&i+t>this.cols;i=n?0:i;var o=e.size.outerWidth&&e.size.outerHeight;return this.horizontalColIndex=o?i+t:this.horizontalColIndex,{col:i,y:this._getColGroupY(i,t)}},n._manageStamp=function(t){var i=e(t),n=this._getElementOffset(t),o=this._getOption("originLeft"),r=o?n.left:n.right,s=r+i.outerWidth,a=Math.floor(r/this.columnWidth);a=Math.max(0,a);var h=Math.floor(s/this.columnWidth);h-=s%this.columnWidth?0:1,h=Math.min(this.cols-1,h);for(var u=this._getOption("originTop"),d=(u?n.top:n.bottom)+i.outerHeight,l=a;h>=l;l++)this.colYs[l]=Math.max(d,this.colYs[l])},n._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var t={height:this.maxY};return this._getOption("fitWidth")&&(t.width=this._getContainerFitWidth()),t},n._getContainerFitWidth=function(){for(var t=0,e=this.cols;--e&&0===this.colYs[e];)t++;return(this.cols-t)*this.columnWidth-this.gutter},n.needsResizeLayout=function(){var t=this.containerWidth;return this.getContainerWidth(),t!=this.containerWidth},i}); -------------------------------------------------------------------------------- /assets/js/fancybox/3.3.5/jquery.fancybox.min.js: -------------------------------------------------------------------------------- 1 | // ================================================== 2 | // fancyBox v3.3.5 3 | // 4 | // Licensed GPLv3 for open source use 5 | // or fancyBox Commercial License for commercial use 6 | // 7 | // http://fancyapps.com/fancybox/ 8 | // Copyright 2018 fancyApps 9 | // 10 | // ================================================== 11 | !function(t,e,n,o){"use strict";function i(t,e){var o,i,a=[],s=0;t&&t.isDefaultPrevented()||(t.preventDefault(),e=t&&t.data?t.data.options:e||{},o=e.$target||n(t.currentTarget),i=o.attr("data-fancybox")||"",i?(a=e.selector?n(e.selector):t.data?t.data.items:[],a=a.length?a.filter('[data-fancybox="'+i+'"]'):n('[data-fancybox="'+i+'"]'),s=a.index(o),s<0&&(s=0)):a=[o],n.fancybox.open(a,e,s))}if(t.console=t.console||{info:function(t){}},n){if(n.fn.fancybox)return void console.info("fancyBox already initialized");var a={loop:!1,gutter:50,keyboard:!0,arrows:!0,infobar:!0,smallBtn:"auto",toolbar:"auto",buttons:["zoom","thumbs","close"],idleTime:3,protect:!1,modal:!1,image:{preload:!1},ajax:{settings:{data:{fancybox:!0}}},iframe:{tpl:'',preload:!0,css:{},attr:{scrolling:"auto"}},defaultType:"image",animationEffect:"zoom",animationDuration:366,zoomOpacity:"auto",transitionEffect:"fade",transitionDuration:366,slideClass:"",baseClass:"",baseTpl:'',spinnerTpl:'
',errorTpl:'

{{ERROR}}

',btnTpl:{download:'',zoom:'',close:'',smallBtn:'',arrowLeft:'',arrowRight:''},parentEl:"body",autoFocus:!1,backFocus:!0,trapFocus:!0,fullScreen:{autoStart:!1},touch:{vertical:!0,momentum:!0},hash:null,media:{},slideShow:{autoStart:!1,speed:4e3},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"},wheel:"auto",onInit:n.noop,beforeLoad:n.noop,afterLoad:n.noop,beforeShow:n.noop,afterShow:n.noop,beforeClose:n.noop,afterClose:n.noop,onActivate:n.noop,onDeactivate:n.noop,clickContent:function(t,e){return"image"===t.type&&"zoom"},clickSlide:"close",clickOutside:"close",dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1,mobile:{idleTime:!1,clickContent:function(t,e){return"image"===t.type&&"toggleControls"},clickSlide:function(t,e){return"image"===t.type?"toggleControls":"close"},dblclickContent:function(t,e){return"image"===t.type&&"zoom"},dblclickSlide:function(t,e){return"image"===t.type&&"zoom"}},lang:"en",i18n:{en:{CLOSE:"Close",NEXT:"Next",PREV:"Previous",ERROR:"The requested content cannot be loaded.
Please try again later.",PLAY_START:"Start slideshow",PLAY_STOP:"Pause slideshow",FULL_SCREEN:"Full screen",THUMBS:"Thumbnails",DOWNLOAD:"Download",SHARE:"Share",ZOOM:"Zoom"},de:{CLOSE:"Schliessen",NEXT:"Weiter",PREV:"Zurück",ERROR:"Die angeforderten Daten konnten nicht geladen werden.
Bitte versuchen Sie es später nochmal.",PLAY_START:"Diaschau starten",PLAY_STOP:"Diaschau beenden",FULL_SCREEN:"Vollbild",THUMBS:"Vorschaubilder",DOWNLOAD:"Herunterladen",SHARE:"Teilen",ZOOM:"Maßstab"}}},s=n(t),r=n(e),c=0,l=function(t){return t&&t.hasOwnProperty&&t instanceof n},d=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),u=function(){var t,n=e.createElement("fakeelement"),i={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(t in i)if(n.style[t]!==o)return i[t];return"transitionend"}(),f=function(t){return t&&t.length&&t[0].offsetHeight},p=function(t,e){var o=n.extend(!0,{},t,e);return n.each(e,function(t,e){n.isArray(e)&&(o[t]=e)}),o},h=function(t,o,i){var a=this;a.opts=p({index:i},n.fancybox.defaults),n.isPlainObject(o)&&(a.opts=p(a.opts,o)),n.fancybox.isMobile&&(a.opts=p(a.opts,a.opts.mobile)),a.id=a.opts.id||++c,a.currIndex=parseInt(a.opts.index,10)||0,a.prevIndex=null,a.prevPos=null,a.currPos=0,a.firstRun=!0,a.group=[],a.slides={},a.addContent(t),a.group.length&&(a.$lastFocus=n(e.activeElement).trigger("blur"),a.init())};n.extend(h.prototype,{init:function(){var i,a,s,r=this,c=r.group[r.currIndex],l=c.opts,d=n.fancybox.scrollbarWidth;n.fancybox.getInstance()||l.hideScrollbar===!1||(n("body").addClass("fancybox-active"),!n.fancybox.isMobile&&e.body.scrollHeight>t.innerHeight&&(d===o&&(i=n('
').appendTo("body"),d=n.fancybox.scrollbarWidth=i[0].offsetWidth-i[0].clientWidth,i.remove()),n("head").append('"),n("body").addClass("compensate-for-scrollbar"))),s="",n.each(l.buttons,function(t,e){s+=l.btnTpl[e]||""}),a=n(r.translate(r,l.baseTpl.replace("{{buttons}}",s).replace("{{arrows}}",l.btnTpl.arrowLeft+l.btnTpl.arrowRight))).attr("id","fancybox-container-"+r.id).addClass("fancybox-is-hidden").addClass(l.baseClass).data("FancyBox",r).appendTo(l.parentEl),r.$refs={container:a},["bg","inner","infobar","toolbar","stage","caption","navigation"].forEach(function(t){r.$refs[t]=a.find(".fancybox-"+t)}),r.trigger("onInit"),r.activate(),r.jumpTo(r.currIndex)},translate:function(t,e){var n=t.opts.i18n[t.opts.lang];return e.replace(/\{\{(\w+)\}\}/g,function(t,e){var i=n[e];return i===o?t:i})},addContent:function(t){var e,i=this,a=n.makeArray(t);n.each(a,function(t,e){var a,s,r,c,l,d={},u={};n.isPlainObject(e)?(d=e,u=e.opts||e):"object"===n.type(e)&&n(e).length?(a=n(e),u=a.data()||{},u=n.extend(!0,{},u,u.options),u.$orig=a,d.src=i.opts.src||u.src||a.attr("href"),d.type||d.src||(d.type="inline",d.src=e)):d={type:"html",src:e+""},d.opts=n.extend(!0,{},i.opts,u),n.isArray(u.buttons)&&(d.opts.buttons=u.buttons),s=d.type||d.opts.type,c=d.src||"",!s&&c&&((r=c.match(/\.(mp4|mov|ogv)((\?|#).*)?$/i))?(s="video",d.opts.videoFormat||(d.opts.videoFormat="video/"+("ogv"===r[1]?"ogg":r[1]))):c.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)?s="image":c.match(/\.(pdf)((\?|#).*)?$/i)?s="iframe":"#"===c.charAt(0)&&(s="inline")),s?d.type=s:i.trigger("objectNeedsType",d),d.contentType||(d.contentType=n.inArray(d.type,["html","inline","ajax"])>-1?"html":d.type),d.index=i.group.length,"auto"==d.opts.smallBtn&&(d.opts.smallBtn=n.inArray(d.type,["html","inline","ajax"])>-1),"auto"===d.opts.toolbar&&(d.opts.toolbar=!d.opts.smallBtn),d.opts.$trigger&&d.index===i.opts.index&&(d.opts.$thumb=d.opts.$trigger.find("img:first")),d.opts.$thumb&&d.opts.$thumb.length||!d.opts.$orig||(d.opts.$thumb=d.opts.$orig.find("img:first")),"function"===n.type(d.opts.caption)&&(d.opts.caption=d.opts.caption.apply(e,[i,d])),"function"===n.type(i.opts.caption)&&(d.opts.caption=i.opts.caption.apply(e,[i,d])),d.opts.caption instanceof n||(d.opts.caption=d.opts.caption===o?"":d.opts.caption+""),"ajax"===d.type&&(l=c.split(/\s+/,2),l.length>1&&(d.src=l.shift(),d.opts.filter=l.shift())),d.opts.modal&&(d.opts=n.extend(!0,d.opts,{infobar:0,toolbar:0,smallBtn:0,keyboard:0,slideShow:0,fullScreen:0,thumbs:0,touch:0,clickContent:!1,clickSlide:!1,clickOutside:!1,dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1})),i.group.push(d)}),Object.keys(i.slides).length&&(i.updateControls(),e=i.Thumbs,e&&e.isActive&&(e.create(),e.focus()))},addEvents:function(){var o=this;o.removeEvents(),o.$refs.container.on("click.fb-close","[data-fancybox-close]",function(t){t.stopPropagation(),t.preventDefault(),o.close(t)}).on("touchstart.fb-prev click.fb-prev","[data-fancybox-prev]",function(t){t.stopPropagation(),t.preventDefault(),o.previous()}).on("touchstart.fb-next click.fb-next","[data-fancybox-next]",function(t){t.stopPropagation(),t.preventDefault(),o.next()}).on("click.fb","[data-fancybox-zoom]",function(t){o[o.isScaledDown()?"scaleToActual":"scaleToFit"]()}),s.on("orientationchange.fb resize.fb",function(t){t&&t.originalEvent&&"resize"===t.originalEvent.type?d(function(){o.update()}):(o.$refs.stage.hide(),setTimeout(function(){o.$refs.stage.show(),o.update()},n.fancybox.isMobile?600:250))}),r.on("focusin.fb",function(t){var o=n.fancybox?n.fancybox.getInstance():null;o.isClosing||!o.current||!o.current.opts.trapFocus||n(t.target).hasClass("fancybox-container")||n(t.target).is(e)||o&&"fixed"!==n(t.target).css("position")&&!o.$refs.container.has(t.target).length&&(t.stopPropagation(),o.focus())}),r.on("keydown.fb",function(t){var e=o.current,i=t.keyCode||t.which;if(e&&e.opts.keyboard&&!(t.ctrlKey||t.altKey||t.shiftKey||n(t.target).is("input")||n(t.target).is("textarea")))return 8===i||27===i?(t.preventDefault(),void o.close(t)):37===i||38===i?(t.preventDefault(),void o.previous()):39===i||40===i?(t.preventDefault(),void o.next()):void o.trigger("afterKeydown",t,i)}),o.group[o.currIndex].opts.idleTime&&(o.idleSecondsCounter=0,r.on("mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle",function(t){o.idleSecondsCounter=0,o.isIdle&&o.showControls(),o.isIdle=!1}),o.idleInterval=t.setInterval(function(){o.idleSecondsCounter++,o.idleSecondsCounter>=o.group[o.currIndex].opts.idleTime&&!o.isDragging&&(o.isIdle=!0,o.idleSecondsCounter=0,o.hideControls())},1e3))},removeEvents:function(){var e=this;s.off("orientationchange.fb resize.fb"),r.off("focusin.fb keydown.fb .fb-idle"),this.$refs.container.off(".fb-close .fb-prev .fb-next"),e.idleInterval&&(t.clearInterval(e.idleInterval),e.idleInterval=null)},previous:function(t){return this.jumpTo(this.currPos-1,t)},next:function(t){return this.jumpTo(this.currPos+1,t)},jumpTo:function(t,e){var i,a,s,r,c,l,d,u=this,p=u.group.length;if(!(u.isDragging||u.isClosing||u.isAnimating&&u.firstRun)){if(t=parseInt(t,10),a=u.current?u.current.opts.loop:u.opts.loop,!a&&(t<0||t>=p))return!1;if(i=u.firstRun=!Object.keys(u.slides).length,!(p<2&&!i&&u.isDragging)){if(r=u.current,u.prevIndex=u.currIndex,u.prevPos=u.currPos,s=u.createSlide(t),p>1&&((a||s.index>0)&&u.createSlide(t-1),(a||s.indexs.pos?"next":"previous"),r.$slide.removeClass("fancybox-slide--complete fancybox-slide--current fancybox-slide--next fancybox-slide--previous"),r.isComplete=!1,e&&(s.isMoved||s.opts.transitionEffect)&&(s.isMoved?r.$slide.addClass(d):(d="fancybox-animated "+d+" fancybox-fx-"+s.opts.transitionEffect,n.fancybox.animate(r.$slide,d,e,function(){r.$slide.removeClass(d).removeAttr("style")}))))}}},createSlide:function(t){var e,o,i=this;return o=t%i.group.length,o=o<0?i.group.length+o:o,!i.slides[t]&&i.group[o]&&(e=n('
').appendTo(i.$refs.stage),i.slides[t]=n.extend(!0,{},i.group[o],{pos:t,$slide:e,isLoaded:!1}),i.updateSlide(i.slides[t])),i.slides[t]},scaleToActual:function(t,e,i){var a,s,r,c,l,d=this,u=d.current,f=u.$content,p=n.fancybox.getTranslate(u.$slide).width,h=n.fancybox.getTranslate(u.$slide).height,g=u.width,b=u.height;!d.isAnimating&&f&&"image"==u.type&&u.isLoaded&&!u.hasError&&(n.fancybox.stop(f),d.isAnimating=!0,t=t===o?.5*p:t,e=e===o?.5*h:e,a=n.fancybox.getTranslate(f),a.top-=n.fancybox.getTranslate(u.$slide).top,a.left-=n.fancybox.getTranslate(u.$slide).left,c=g/a.width,l=b/a.height,s=.5*p-.5*g,r=.5*h-.5*b,g>p&&(s=a.left*c-(t*c-t),s>0&&(s=0),sh&&(r=a.top*l-(e*l-e),r>0&&(r=0),rc/a?l=c/a:c>l*a&&(c=l*a)),d.width=c,d.height=l,d)},update:function(){var t=this;n.each(t.slides,function(e,n){t.updateSlide(n)})},updateSlide:function(t,e){var o=this,i=t&&t.$content,a=t.width||t.opts.width,s=t.height||t.opts.height;i&&(a||s||"video"===t.contentType)&&!t.hasError&&(n.fancybox.stop(i),n.fancybox.setTranslate(i,o.getFitPos(t)),t.pos===o.currPos&&(o.isAnimating=!1,o.updateCursor())),t.$slide.trigger("refresh"),o.$refs.toolbar.toggleClass("compensate-for-scrollbar",t.$slide.get(0).scrollHeight>t.$slide.get(0).clientHeight),o.trigger("onUpdate",t)},centerSlide:function(t,e){var i,a,s=this;s.current&&(i=Math.round(t.$slide.width()),a=t.pos-s.current.pos,n.fancybox.animate(t.$slide,{top:0,left:a*i+a*t.opts.gutter,opacity:1},e===o?0:e,null,!1))},updateCursor:function(t,e){var o,i=this,a=i.current,s=i.$refs.container.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-drag fancybox-can-zoomOut");a&&!i.isClosing&&(o=i.isZoomable(),s.toggleClass("fancybox-is-zoomable",o),n("[data-fancybox-zoom]").prop("disabled",!o),o&&("zoom"===a.opts.clickContent||n.isFunction(a.opts.clickContent)&&"zoom"===a.opts.clickContent(a))?i.isScaledDown(t,e)?s.addClass("fancybox-can-zoomIn"):a.opts.touch?s.addClass("fancybox-can-drag"):s.addClass("fancybox-can-zoomOut"):a.opts.touch&&"video"!==a.contentType&&s.addClass("fancybox-can-drag"))},isZoomable:function(){var t,e=this,n=e.current;if(n&&!e.isClosing&&"image"===n.type&&!n.hasError){if(!n.isLoaded)return!0;if(t=e.getFitPos(n),n.width>t.width||n.height>t.height)return!0}return!1},isScaledDown:function(t,e){var i=this,a=!1,s=i.current,r=s.$content;return t!==o&&e!==o?a=t1||Math.abs(t.height()-n.height)>1),n},loadSlide:function(t){var e,o,i,a=this;if(!t.isLoading&&!t.isLoaded){switch(t.isLoading=!0,a.trigger("beforeLoad",t),e=t.type,o=t.$slide,o.off("refresh").trigger("onReset").addClass(t.opts.slideClass),e){case"image":a.setImage(t);break;case"iframe":a.setIframe(t);break;case"html":a.setContent(t,t.src||t.content);break;case"video":a.setContent(t,'