Files
thetempusproject/app/plugins/bookmarks/models/bookmarks.php
2024-08-04 21:15:59 -04:00

706 lines
25 KiB
PHP

<?php
/**
* app/plugins/bookmarks/models/bookmarks.php
*
* This class is used for the manipulation of the bookmarks database table.
*
* @package TP Bookmarks
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @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\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 = '<i class="glyphicon glyphicon-link"></i>';
} else {
if (strpos($instance->icon, 'http') !== false) {
$instance->iconHtml = '<img src="' . $instance->icon .'" />';
} else {
$instance->iconHtml = '<img src="' . $base_url . ltrim( $instance->icon, '/' ) .'" />';
}
}
if ( empty( $instance->hiddenAt ) ) {
$instance->hideBtn = '
<a href="{ROOT_URL}bookmarks/hideBookmark/'.$instance->ID.'" class="btn btn-sm btn-warning" role="button">
<i class="glyphicon glyphicon-eye-open"></i>
</a>';
} else {
$instance->hideBtn = '
<a href="{ROOT_URL}bookmarks/showBookmark/'.$instance->ID.'" class="btn btn-sm btn-default" role="button">
<i class="glyphicon glyphicon-eye-open"></i>
</a>';
}
if ( empty( $instance->archivedAt ) ) {
$instance->archiveBtn = '
<a href="{ROOT_URL}bookmarks/archiveBookmark/'.$instance->ID.'" class="btn btn-sm btn-info" role="button">
<i class="glyphicon glyphicon-briefcase"></i>
</a>';
} else {
$instance->archiveBtn = '
<a href="{ROOT_URL}bookmarks/unarchiveBookmark/'.$instance->ID.'" class="btn btn-sm btn-default" role="button">
<i class="glyphicon glyphicon-briefcase"></i>
</a>';
}
if ( ! empty( $instance->refreshedAt ) && time() < ( $instance->refreshedAt + ( 60 * 10 ) ) ) {
$instance->refreshBtn = '
<a href="{ROOT_URL}bookmarks/refreshBookmark/'.$instance->ID.'" class="btn btn-sm btn-danger" role="button">
<i class="glyphicon glyphicon-refresh"></i>
</a>';
} else {
$instance->refreshBtn = '
<a href="{ROOT_URL}bookmarks/refreshBookmark/'.$instance->ID.'" class="btn btn-sm btn-success" role="button">
<i class="glyphicon glyphicon-refresh"></i>
</a>';
}
$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;
}
}