* @link https://TheTempusProject.com * @license https://opensource.org/licenses/MIT [MIT LICENSE] */ namespace TheTempusProject\Models; use TheTempusProject\Bedrock\Classes\Config; use TheTempusProject\Bedrock\Functions\Check; use TheTempusProject\Canary\Bin\Canary as Debug; use TheTempusProject\Classes\DatabaseModel; use TheTempusProject\TheTempusProject as App; use TheTempusProject\Houdini\Classes\Filters; use TheTempusProject\Bedrock\Classes\CustomException; class Bookmarks extends DatabaseModel { public $tableName = 'bookmarks'; public $linkTypes = [ 'Open in New Tab' => 'external', 'Open in Same Tab' => 'internal', ]; public $databaseMatrix = [ [ 'title', 'varchar', '256' ], [ 'url', 'text', '' ], [ 'color', 'varchar', '48' ], [ 'privacy', 'varchar', '48' ], [ 'folderID', 'int', '11' ], [ 'description', 'text', '' ], [ 'createdBy', 'int', '11' ], [ 'createdAt', 'int', '11' ], [ 'meta', 'text', '' ], [ 'icon', 'text', '' ], [ 'archivedAt', 'int', '11' ], [ 'refreshedAt', 'int', '11' ], [ 'hiddenAt', 'int', '11' ], [ 'order', 'int', '11' ], [ 'linkType', 'varchar', '32' ], ]; /** * The model constructor. */ public function __construct() { parent::__construct(); } public function create( $title, $url, $folderID = 0, $description = '', $color = 'default', $privacy = 'private', $type = 'external' ) { $fields = [ 'title' => $title, 'url' => $url, 'description' => $description, 'color' => $color, 'privacy' => $privacy, 'createdBy' => App::$activeUser->ID, 'createdAt' => time(), ]; if ( !empty( $folderID ) ) { $fields['folderID'] = $folderID; } else { $fields['folderID'] = null; } if ( ! self::$db->insert( $this->tableName, $fields ) ) { new CustomException( 'bookmarkCreate' ); Debug::error( "Bookmarks: not created " . var_export($fields,true) ); return false; } return self::$db->lastId(); } public function update( $id, $title, $url, $folderID = 0, $description = '', $color = 'default', $privacy = 'private', $type = 'external', $order = 0 ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $fields = [ 'title' => $title, 'url' => $url, 'description' => $description, 'color' => $color, 'privacy' => $privacy, // 'linkType' => $type, // 'order' => $order, ]; if ( !empty( $folderID ) ) { $fields['folderID'] = $folderID; } if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } public function byUser( $limit = null ) { $whereClause = ['createdBy', '=', App::$activeUser->ID]; if ( empty( $limit ) ) { $bookmarks = self::$db->get( $this->tableName, $whereClause ); } else { $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); } if ( !$bookmarks->count() ) { Debug::info( 'No Bookmarks found.' ); return false; } return $this->filter( $bookmarks->results() ); } public function byFolder( $id, $limit = null ) { $whereClause = ['createdBy', '=', App::$activeUser->ID, 'AND']; $whereClause = array_merge( $whereClause, [ 'folderID', '=', $id ] ); if ( empty( $limit ) ) { $bookmarks = self::$db->get( $this->tableName, $whereClause ); } else { $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] ); } if ( !$bookmarks->count() ) { Debug::info( 'No Bookmarks found.' ); return false; } return $this->filter( $bookmarks->results() ); } public function noFolder( $id = 0, $limit = 10 ) { $whereClause = ['createdBy', '=', App::$activeUser->ID, 'AND']; if ( !empty( $id ) ) { $whereClause = array_merge( $whereClause, ['folderID', '!=', $id] ); } else { $whereClause = array_merge( $whereClause, [ 'folderID', 'IS', null] ); } if ( empty( $limit ) ) { $bookmarks = self::$db->get( $this->tableName, $whereClause ); } else { $bookmarks = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [ 0, $limit ] ); } if ( !$bookmarks->count() ) { Debug::info( 'No Bookmarks found.' ); return false; } return $this->filter( $bookmarks->results() ); } public function getName( $id ) { $bookmarks = self::findById( $id ); if (false == $bookmarks) { return 'unknown'; } return $bookmarks->title; } public function getColor( $id ) { $bookmarks = self::findById( $id ); if (false == $bookmarks) { return 'default'; } return $bookmarks->color; } public function simpleByUser() { $whereClause = ['createdBy', '=', App::$activeUser->ID]; $bookmarks = self::$db->get( $this->tableName, $whereClause ); if ( !$bookmarks->count() ) { Debug::warn( 'Could not find any bookmarks' ); return false; } $bookmarks = $bookmarks->results(); $out = []; foreach ( $bookmarks as $bookmarks ) { $out[ $bookmarks->title ] = $bookmarks->ID; } return $out; } public function simpleObjectByUser() { $whereClause = ['createdBy', '=', App::$activeUser->ID]; $bookmarks = self::$db->get( $this->tableName, $whereClause ); if ( !$bookmarks->count() ) { Debug::warn( 'Could not find any bookmarks' ); return false; } $bookmarks = $bookmarks->results(); $out = []; foreach ( $bookmarks as $bookmarks ) { $obj = new \stdClass(); $obj->title = $bookmarks->title; $obj->ID = $bookmarks->ID; $out[] = $obj; } return $out; } public function deleteByFolder( $folderID ) { $whereClause = [ 'createdBy', '=', App::$activeUser->ID, 'AND' ]; $whereClause = array_merge( $whereClause, [ 'folderID', '=', $folderID ] ); $bookmarks = self::$db->get( $this->tableName, $whereClause ); if ( ! $bookmarks->count() ) { Debug::info( 'No ' . $this->tableName . ' data found.' ); return []; } foreach( $bookmarks->results() as $bookmark ) { $this->delete( $bookmark->ID ); } return true; } private function resolveShortenedUrl( $url ) { $ch = curl_init($url); // Set curl options curl_setopt($ch, CURLOPT_NOBODY, true); // We don't need the body curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set a timeout // curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // Maybe sketchy? // Maybe sketchy? // curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable SSL host verification // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL peer verification // curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); // added to support the regex site // curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // = // curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'AES256+EECDH:AES256+EDH' ); // Execute curl $response = curl_exec( $ch ); // Check if there was an error if ( curl_errno( $ch ) ) { // Get error details $errorCode = curl_errno($ch); $errorMessage = curl_error($ch); // Log or display the error details dv('cURL Error: ' . $errorMessage . ' (Error Code: ' . $errorCode . ')'); curl_close($ch); // return $url; // Return the original URL if there was an error $url = rtrim( $url, '/' ); $ch2 = curl_init($url); curl_setopt($ch2, CURLOPT_NOBODY, true); // We don't need the body curl_setopt($ch2, CURLOPT_FOLLOWLOCATION, true); // Follow redirects curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); // Return the response curl_setopt($ch2, CURLOPT_TIMEOUT, 5); // Set a timeout curl_exec($ch2); if ( curl_errno( $ch2 ) ) { } curl_close( $ch ); return $url; } // Get the effective URL (the final destination after redirects) $finalUrl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL ); curl_close( $ch ); return $finalUrl; // $headers = get_headers( $url, 1 ); $headers = @get_headers($url, 1); if ( $headers === false ) { } if ( isset( $headers['Location'] ) ) { if (is_array($headers['Location'])) { return end($headers['Location']); } else { return $headers['Location']; } } else { return $url; } } public function filter( $data, $params = [] ) { foreach ( $data as $instance ) { if ( !is_object( $instance ) ) { $instance = $data; $end = true; } $base_url = $this->getBaseUrl( $instance->url ); if ( empty( $instance->icon ) ) { $instance->iconHtml = ''; } else { if (strpos($instance->icon, 'http') !== false) { $instance->iconHtml = ''; } else { $instance->iconHtml = ''; } } if ( empty( $instance->hiddenAt ) ) { $instance->hideBtn = ' '; } else { $instance->hideBtn = ' '; } if ( empty( $instance->archivedAt ) ) { $instance->archiveBtn = ' '; } else { $instance->archiveBtn = ' '; } if ( ! empty( $instance->refreshedAt ) && time() < ( $instance->refreshedAt + ( 60 * 10 ) ) ) { $instance->refreshBtn = ' '; } else { $instance->refreshBtn = ' '; } $out[] = $instance; if ( !empty( $end ) ) { $out = $out[0]; break; } } return $out; } public function hide( $id ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $fields = [ 'hiddenAt' => time(), ]; if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } public function show( $id ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $fields = [ 'hiddenAt' => 0, ]; if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } public function archive( $id ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $fields = [ 'archivedAt' => time(), ]; if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } public function unarchive( $id ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $fields = [ 'archivedAt' => 0, ]; if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } public function extractMetaTags($htmlContent) { $doc = new \DOMDocument(); @$doc->loadHTML($htmlContent); $metaTags = []; foreach ($doc->getElementsByTagName('meta') as $meta) { $name = $meta->getAttribute('name') ?: $meta->getAttribute('property'); $content = $meta->getAttribute('content'); if ($name && $content) { $metaTags[$name] = $content; } } return $metaTags; } public function fetchUrlData($url) { $ch = curl_init(); // Set cURL options curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); // Include headers in the output curl_setopt($ch, CURLOPT_NOBODY, false); // Include the body in the output curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set a timeout curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable SSL host verification for testing curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL peer verification for testing // Execute cURL request $response = curl_exec($ch); // Check if there was an error if (curl_errno($ch)) { $errorMessage = curl_error($ch); curl_close($ch); throw new \Exception('cURL Error: ' . $errorMessage); } // Get HTTP status code $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Separate headers and body $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $headers = substr($response, 0, $headerSize); $body = substr($response, $headerSize); curl_close($ch); // Parse headers into an associative array $headerLines = explode("\r\n", trim($headers)); $headerArray = []; foreach ($headerLines as $line) { $parts = explode(': ', $line, 2); if (count($parts) == 2) { $headerArray[$parts[0]] = $parts[1]; } } return [ 'http_code' => $httpCode, 'headers' => $headerArray, 'body' => $body, ]; } private function getMetaTagsAndFavicon( $url ) { try { // $url = 'https://runescape.wiki'; $data = $this->fetchUrlData($url); // iv($data); // Get headers $headers = $data['headers']; iv($headers); // Get meta tags $metaTags = $this->extractMetaTags($data['body']); } catch (Exception $e) { dv( 'Error: ' . $e->getMessage()); } $metaInfo = [ 'url' => $url, 'title' => null, 'description' => null, 'image' => null, 'favicon' => null ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $html = curl_exec($ch); curl_close($ch); if ($html === false) { return null; } $meta = @get_meta_tags( $url ); $dom = new \DOMDocument('1.0', 'utf-8'); $dom->strictErrorChecking = false; $dom->loadHTML($html, LIBXML_NOERROR); $xml = simplexml_import_dom($dom); $arr = $xml->xpath('//link[@rel="shortcut icon"]'); // Get the title of the page $titles = $dom->getElementsByTagName('title'); if ($titles->length > 0) { $metaInfo['title'] = $titles->item(0)->nodeValue; } // Get the meta tags $metaTags = $dom->getElementsByTagName('meta'); $metadata = []; foreach ($metaTags as $meta) { $metadata[] = [ 'name' => $meta->getAttribute('name'), 'property' => $meta->getAttribute('property'), 'content' => $meta->getAttribute('content') ]; if ($meta->getAttribute('name') === 'description') { $metaInfo['description'] = $meta->getAttribute('content'); } if ($meta->getAttribute('itemprop') === 'image') { $metaInfo['google_image'] = $meta->getAttribute('content'); } if ($meta->getAttribute('property') === 'og:image') { $metaInfo['image'] = $meta->getAttribute('content'); } } // Get the link tags to find the favicon $linkTags = $dom->getElementsByTagName('link'); $metadata['links'] = []; foreach ($linkTags as $link) { $metadata['links'][] = [ $link->getAttribute('rel') => $link->getAttribute('href') ]; if ( $link->getAttribute('rel') === 'icon' || $link->getAttribute('rel') === 'shortcut icon') { $metaInfo['favicon'] = $link->getAttribute('href'); break; } } $metaInfo['metadata'] = $metadata; return $metaInfo; } public function retrieveInfo( $url ) { $finalDestination = $this->resolveShortenedUrl( $url ); $info = $this->getMetaTagsAndFavicon( $finalDestination ); $base_url = $this->getBaseUrl( $finalDestination ); if ( ! empty( $info['favicon'] ) ) { echo 'favicon exists' . PHP_EOL; if ( stripos( $info['favicon'], 'http' ) !== false) { echo 'favicon is full url' . PHP_EOL; $imageUrl = $info['favicon']; } else { echo 'favicon is not full url' . PHP_EOL; $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $info['favicon'], '/' ); } if ( $this->isValidImageUrl( $imageUrl ) ) { echo 'image is valid' . PHP_EOL; $info['favicon'] = $imageUrl; } else { echo 'image is not valid' . PHP_EOL; $base_info = $this->getMetaTagsAndFavicon( $base_url ); if ( ! empty( $base_info['favicon'] ) ) { echo 'parent favicon exists!'; if ( stripos( $base_info['favicon'], 'http' ) !== false) { echo 'parent favicon is full url' . PHP_EOL; $imageUrl = $base_info['favicon']; } else { echo 'parent favicon is not full url' . PHP_EOL; $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $base_info['favicon'], '/' ); } if ( $this->isValidImageUrl( $imageUrl ) ) { echo 'parent favicon image is valid' . PHP_EOL; $info['favicon'] = $imageUrl; } else { echo 'parent favicon image is not valid' . PHP_EOL; } } } } else { echo 'favicon does not exist' . PHP_EOL; $base_info = $this->getMetaTagsAndFavicon( $base_url ); if ( ! empty( $base_info['favicon'] ) ) { echo 'parent favicon exists!' . PHP_EOL; if ( stripos( $base_info['favicon'], 'http' ) !== false) { echo 'parent favicon is full url' . PHP_EOL; $imageUrl = $base_info['favicon']; } else { echo 'parent favicon is not full url' . PHP_EOL; $imageUrl = trim( $base_url, '/' ) . '/' . ltrim( $base_info['favicon'], '/' ); } if ( $this->isValidImageUrl( $imageUrl ) ) { echo 'parent favicon image is valid' . PHP_EOL; $info['favicon'] = $imageUrl; } else { echo 'parent favicon image is not valid' . PHP_EOL; } } } return $info; } public function refreshInfo( $id ) { if ( !Check::id( $id ) ) { Debug::info( 'Bookmarks: illegal ID.' ); return false; } $bookmark = self::findById( $id ); if ( $bookmark == false ) { Debug::info( 'Bookmarks not found.' ); return false; } if ( $bookmark->createdBy != App::$activeUser->ID ) { Debug::info( 'You do not have permission to modify this bookmark.' ); return false; } if ( time() < ( $bookmark->refreshedAt + ( 60 * 10 ) ) ) { Debug::info( 'You may only fetch bookmarks once every 10 minutes.' ); return false; } $info = $this->retrieveInfo( $bookmark->url ); $fields = [ // 'refreshedAt' => time(), ]; if ( empty( $bookmark->title ) && ! empty( $info['title'] ) ) { $fields['title'] = $info['title']; } if ( empty( $bookmark->description ) && ! empty( $info['description'] ) ) { $fields['description'] = $info['description']; } if ( ( empty( $bookmark->icon ) || ! $this->isValidImageUrl( $bookmark->icon ) ) && ! empty( $info['favicon'] ) ) { $fields['icon'] = $info['favicon']; } $fields['meta'] = json_encode( $info['metadata'] ); if ( !self::$db->update( $this->tableName, $id, $fields ) ) { new CustomException( 'bookmarkUpdate' ); Debug::error( "Bookmarks: $id not updated" ); return false; } return true; } private function getBaseUrl ($url ) { $parsedUrl = parse_url($url); if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) { return $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . '/'; } else { return null; // URL is not valid or cannot be parsed } } function isValidImageUrl($url) { $headers = @get_headers($url); if ($headers && strpos($headers[0], '200') !== false) { return true; // Further check to ensure it's an image foreach ($headers as $header) { if (strpos(strtolower($header), 'content-type:') !== false) { if (strpos(strtolower($header), 'image/') !== false) { return true; } } } } return false; } }