diff --git a/README.md b/README.md index 939a51b..f8c16d3 100644 --- a/README.md +++ b/README.md @@ -29,26 +29,21 @@ In addition to shortening URLs (based on domain that you use), SmartyURL also of * **Tracking and analytics** * **Customization** -## Server Requirements +## Installation -- You need a web hosting account (for a domain or sub-domain) with PHP 7.4 or higher support and the following PHP extensions (typically supported by most PHP hosting providers): +Currently, as SmartyURL is in its early stages, you can only install it using Composer. Once we release the first official version of SmartyURL, we will offer detailed installation instructions for other methods. - - [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) , [intl](http://php.net/manual/en/intl.requirements.php) , [mbstring](http://php.net/manual/en/mbstring.installation.php) , [libcurl](https://www.php.net/manual/en/curl.setup.php) , [gmp](https://www.php.net/manual/en/gmp.installation.php) , [json](https://www.php.net/manual/en/json.installation.php), [bcmath](https://www.php.net/manual/en/bc.setup.php) - -- Your web hosting account should have MySQL 8.0+ support +see [Developers Guide](docs/developers.md#installation) for more information about how to install SmartyURL. -Afterwards, you can refer to the [installation instructions](_docs/installation.md) to set up the tool on your hosting account and begin using it. +Certainly, please refer to the [documentation](docs/index.md) for detailed instructions How configure, and effectively use SmartyURL for comprehensive guidance. -Certainly, please refer to the [documentation](_docs/index.md) for detailed instructions on how to install, configure, and effectively use Smart URL for comprehensive guidance. - -**Visitors IP Country detection** - -SmartyURL uses the `ip2location/ip2location-php` library to determine visitors country based on their IP addresses. It includes the free "IP2Location™ LITE IP-COUNTRY Database" for both personal and commercial use. For enhanced geographical redirect conditions with more accurate and up-to-date IP-based country data or if you need more accuracy consider purchasing a licensed IP2Location database. Refer to [IP2Location Database Docs](_docs/ip2location.md) for more details. +## Documentation +Please take a look to SmartyURL [documentation](docs/index.md) for detailed installation, configuration, and usage instructions. -## Documentation +**Visitors IP Country detection** -Please take a look to SmartyURL [documentation](_docs/index.md) for detailed installation, configuration, and usage instructions. +SmartyURL uses the `ip2location/ip2location-php` library to determine visitors country based on their IP addresses. It includes the free "IP2Location™ LITE IP-COUNTRY Database" for both personal and commercial use. For enhanced geographical redirect conditions with more accurate and up-to-date IP-based country data or if you need more accuracy consider purchasing a licensed IP2Location database. Refer to [IP2Location Database Docs](docs/ip2location.md) for more details. ## License @@ -68,4 +63,4 @@ Also We would like to acknowledge the following resources and contributors for t ## SmartyURL Legal Notice -For more information, please refer to the [Legal Notice](_docs/legalnotice.md). +For more information, please refer to the [Legal Notice](docs/legalnotice.md). diff --git a/_docs/developers.md b/_docs/developers.md deleted file mode 100644 index 880506c..0000000 --- a/_docs/developers.md +++ /dev/null @@ -1,41 +0,0 @@ -# SmartyURL Developer Guide - -## Installation - -Currently, as SmartyURL is in its early stages, you can only install it using Composer. Once we release the first official version of SmartyURL, we will offer detailed installation instructions for other methods. - -install SmartyURL using composer: - -```cli -composer create-project extendy/smartyurl myapp -cd myapp -composer install -cp env .env -``` - -Ensure that you've created a MySQL database, then proceed to edit the .env file. Update the database configuration and make any necessary changes to tailor the other settings to your specific requirements. - -then run the migrate all command to import database: - -```cli -php spark migrate --all -``` -Then you need to create the first user: - -You can create a new user by running: - -```cli -php spark shield:user create -``` - -or by visiting your website and register new user - -Ensure the user you've created is designated as a superadmin by modifying the `auth_groups_users` database table. Set the user's group name to 'superadmin' instead of 'user' for the created user. - -Afterward, you can disable new user registration by editing the .env file. Make sure to set `Auth.allowRegistration` to 'false'. If it's not already present in your .env file, you can add it like this: - -When logged in, you might be prompted to verify your email to activate your account. Please check your email for a verification link. If you are unable to access your email, you can manually set the 'active' value to 1 in the 'users' database table for the user you've created. - -```cli -Auth.allowRegistration = false -``` diff --git a/app/Config/AuthGroups.php b/app/Config/AuthGroups.php index a3506a0..e7fea64 100644 --- a/app/Config/AuthGroups.php +++ b/app/Config/AuthGroups.php @@ -58,9 +58,9 @@ class AuthGroups extends ShieldAuthGroups 'super.admin' => 'Does he super admin', 'admin.manageotherurls' => 'can manage other users URLs', // not used yet // url - 'url.access' => 'Can Access URLs', // not used yet + 'url.access' => 'Can Access URLs', 'url.new' => 'Can Create a new URL', - 'url.manage' => 'Can Manage URL (edit , delete)', + 'url.manage' => 'Can Manage his/her URLs only (edit , delete)', ]; /** diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 9474a19..3cd3ae6 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -26,6 +26,8 @@ $routes->post('new', 'Url::newAction', ['filter' => 'session']); $routes->get('edit/(:num)', 'Url::edit/$1', ['filter' => 'session']); $routes->post('edit/(:num)', 'Url::editAction/$1', ['filter' => 'session']); + $routes->get('hits/(:num)', 'Url::hitslist/$1', ['filter' => 'session']); + $routes->get('qrcode/(:num)', 'Url::generateQRCode/$1', ['filter' => 'session']); }); // language route diff --git a/app/Config/Smarty.php b/app/Config/Smarty.php index db98e8d..6892c14 100644 --- a/app/Config/Smarty.php +++ b/app/Config/Smarty.php @@ -8,7 +8,7 @@ class Smarty extends BaseConfig { public $smarty_name = 'SmartyURL'; public $smarty_online_repo = 'https://smartyurl.extendy.net'; - public $smarty_version = '0.0.0-dev-DND'; + public $smarty_version = '0.0.0-dev-DND-1'; /** * @var string contain the file name of jquery supported version eg jquery-3.7.1 without js diff --git a/app/Config/Smartyurl.php b/app/Config/Smartyurl.php index da89e59..c92f7de 100644 --- a/app/Config/Smartyurl.php +++ b/app/Config/Smartyurl.php @@ -87,6 +87,20 @@ class Smartyurl extends BaseConfig */ public int $maxUrlListPerPage = 100; + /** + * QR code version depends on your specific requirements and use case. + * QR codes come in various versions, ranging from Version 1 to Version 40 + * best from 3 to 10 + * default is 7 and if you have any error while generating QR Codes you can + * increase it. + * 7 can handle 528 bit (each utf-8 char is 8 bit) + * + * @see https://www.qrcode.com/en/about/version.html + * + * @var int + */ + public $qrCodeVersion = 7; + /** * The allowed pattern for Url Identifier * diff --git a/app/Controllers/Assist.php b/app/Controllers/Assist.php index 3eca04f..469dddc 100644 --- a/app/Controllers/Assist.php +++ b/app/Controllers/Assist.php @@ -570,7 +570,7 @@ function format(d) { "dom": 'lfrtipB', "processing": true, "serverSide": true, - responsive: true, + "responsive": true, order: [[0, 'desc']], "pageLength": {$defautltUrlListPerPage}, "columnDefs": [ diff --git a/app/Controllers/Url.php b/app/Controllers/Url.php index 56b70e7..3c432f1 100644 --- a/app/Controllers/Url.php +++ b/app/Controllers/Url.php @@ -2,9 +2,13 @@ namespace App\Controllers; +use App\Models\UrlHitsModel; use App\Models\UrlModel; use App\Models\UrlTagsDataModel; use App\Models\UrlTagsModel; +use chillerlan\QRCode\Output\QROutputInterface; +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\QROptions; use Extendy\Smartyurl\SmartyUrl; use Extendy\Smartyurl\UrlConditions; use Extendy\Smartyurl\UrlIdentifier; @@ -25,6 +29,7 @@ public function __construct() $this->urltagsdatamodel = new UrlTagsDataModel(); $this->urlmodel = new UrlModel(); $this->urltags = new UrlTags(); + $this->urlhitsmodel = new UrlHitsModel(); } /** @@ -236,7 +241,7 @@ public function listData() if ($result->url_title === '') { $urlTitle = lang('Url.UrlTitleNoTitle'); } else { - $urlTitle = $result->url_title; + $urlTitle = esc($result->url_title); } // i will get the url tags $url_tags_json = $this->urltags->getUrlTagsCloud($result->url_id); @@ -259,9 +264,9 @@ public function listData() } else { $url_owner = ''; } - + $result->url_identifier = esc($result->url_identifier); // $result->url_id],$result->url_title,$result->url_hitscounter - $Go_Url = smarty_detect_site_shortlinker() . $result->url_identifier; + $Go_Url = esc(smarty_detect_site_shortlinker() . $result->url_identifier); $records[] = [ 'url_id_col' => $result->url_id, 'url_identifier_col' => "{$result->url_identifier} @@ -291,7 +296,86 @@ public function listData() public function view($UrlId) { - d($UrlId); + if (! auth()->user()->can('url.access', 'admin.manageotherurls', 'super.admin')) { + return smarty_permission_error(); + } + + $UrlModel = new UrlModel(); + $UrlTags = new UrlTags(); + $url_id = (int) esc(smarty_remove_whitespace_from_url_identifier($UrlId)); + if ($url_id === 0) { + // url_id given is not valid id + return redirect()->to('dashboard')->with('notice', lang('Url.urlError')); + } + $urlData = $UrlModel->where('url_id', $url_id)->first(); + + if ($urlData === null) { + // url not exsists in dataase + return redirect()->to('dashboard')->with('error', lang('Url.urlNotFoundShort')); + } + + // i will check the user permission , does he allowed to access this url info + $userCanAccessUrl = $this->smartyurl->userCanAccessUrlInfo($url_id, (int) $urlData['url_user_id']); + if (! $userCanAccessUrl) { + return smarty_permission_error('It not your URL 😉😉😉'); + } + $urlTagsCloud = $UrlTags->getUrlTagsCloud($url_id); + // $urlTagsCloud = '[{"value":"tag1","tag_id":"3"},{"value":"tag2","tag_id":"27"},{"value":"tag3","tag_id":"24"}]'; + + $Go_Url = esc(smarty_detect_site_shortlinker() . $urlData['url_identifier']); + $url_owner_username = smarty_get_user_username($urlData['url_user_id']); + + $data = []; + $data['url_id'] = $urlData['url_id']; + if ($urlData['url_title'] === '') { + $urlData['url_title'] = lang('Url.UrlTitleNoTitle'); + } + $data['url_owner_username'] = $url_owner_username; + $data['url_title'] = esc($urlData['url_title']); + $data['url_targeturl'] = esc($urlData['url_targeturl']); + $data['url_identifier'] = esc($urlData['url_identifier']); + $data['url_hitscounter'] = $urlData['url_hitscounter']; + + $data['created_at'] = $urlData['created_at']; + $data['updated_at'] = $urlData['updated_at']; + $data['go_url'] = $Go_Url; + + $data['url_tags'] = json_decode($urlTagsCloud); + + // i will get the redirect conditions + $redirectConditions = json_decode($urlData['url_conditions']); + $data['condition'] = null; + $data['condition_text'] = null; + + if ($redirectConditions !== null) { + // there is a url redirect condition + $data['condition'] = $redirectConditions->condition; + + switch ($data['condition']) { + case 'location': + $data['condition_text'] = lang('Url.ByvisitorsGeolocation'); + break; + + case 'device': + $data['condition_text'] = lang('Url.ByvisitorsDevice'); + break; + + default: + $data['condition_text'] = $data['condition']; + } + + $data['conditions'] = $redirectConditions->conditions; + } else { + $data['condition_text'] = lang('Url.urlInfoNoRecdirectCondition'); + } + + // dd($redirectConditions); + + // i will try to get the last 25 hits of the url + $lasthits = $this->urlhitsmodel->getLast25Hits($urlData['url_id']); + $data['lasthits'] = $lasthits; + + return view(smarty_view('url/urlinfo'), $data); } public function new() @@ -558,7 +642,7 @@ public function editAction($UrlId) } // urlTitle - $urlTitle = esc($this->request->getPost('UrlTitle')); + $urlTitle = $this->request->getPost('UrlTitle'); $redirectCondition = esc($this->request->getPost('redirectCondition')); if ($redirectCondition === 'device' || $redirectCondition === 'geolocation') { // url_conditions @@ -643,10 +727,85 @@ public function editAction($UrlId) } // return redirect()->to("url/edit/{$UrlId}")->withInput()->with('error', lang('OK'))->with('updated',"yes"); - return redirect()->back()->with('success', lang('Url.UpdateURLOK')); + return redirect()->to("url/view/{$UrlId}")->with('success', lang('Url.UpdateURLOK')); } // updated error return redirect()->to("url/edit/{$UrlId}")->withInput()->with('error', lang('Url.UpdateURLError')); } + + public function hitslist($UrlId) + { + echo 'URL HITS OF.' . $UrlId; + } + + public function generateQRCode($UrlId) + { + // set response type + $response = service('response'); + $response->setContentType('image/svg+xml'); + + $error = ''; + if (! auth()->user()->can('url.access', 'admin.manageotherurls', 'super.admin')) { + $error = 'Permission error'; + + return $response->setBody(smarty_svg_error($error)); + } + + $UrlModel = new UrlModel(); + $url_id = (int) esc(smarty_remove_whitespace_from_url_identifier($UrlId)); + + if ($url_id === 0) { + // url_id given is not valid id + $error = lang('Url.urlError'); + + return $response->setBody(smarty_svg_error($error)); + } + $urlData = $UrlModel->where('url_id', $url_id)->first(); + + if ($urlData === null) { + // url not exsists in dataase + $error = lang('Url.urlNotFoundShort'); + + return $response->setBody(smarty_svg_error($error)); + } + + // i will check the user permission , does he allowed to access this url info + $userCanAccessUrl = $this->smartyurl->userCanAccessUrlInfo($url_id, (int) $urlData['url_user_id']); + if (! $userCanAccessUrl) { + $error = 'not your URL 😉'; + + return $response->setBody(smarty_svg_error($error)); + } + + $Go_Url = esc(smarty_detect_site_shortlinker() . $urlData['url_identifier']); + + // prepare for the filename + // remove any special chars and white spaces will be _ + $pattern = '/[^\w\d\.,;!?@#$%^&*()_+-=:<>"\'\/\\\[\]{}|`~]+/u'; + $filename = setting('Smartyurl.siteName') . "_{$UrlId}.svg"; + $filename = str_replace(' ', '_', $filename); + $filename = preg_replace($pattern, '', $filename); + + // if query download i will set Content Disposition to attachment + $download = (int) $this->request->getGet('download'); // Access the 'download' parameter + if ($download === 1) { + $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } else { + $response->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"'); + } + + // now I will generate QR Code + $options = new QROptions(); + // i will smarty detect qr version by text length + $options->version = smarty_smart_detect_qrversion($Go_Url); + $options->outputType = QROutputInterface::MARKUP_SVG; + $options->outputBase64 = false; + $options->drawLightModules = true; + $options->circleRadius = 0.4; + $out = (new QRCode($options))->render($Go_Url); + + // now i will return the image + return $response->setBody($out); + } } diff --git a/app/Helpers/smarty_helper.php b/app/Helpers/smarty_helper.php index 467dcfc..381caa5 100644 --- a/app/Helpers/smarty_helper.php +++ b/app/Helpers/smarty_helper.php @@ -197,3 +197,20 @@ function smarty_get_user_username($userId) return $username; } } + +if (! function_exists('smarty_smart_detect_qrversion')) { + function smarty_smart_detect_qrversion($url) + { + return setting('Smartyurl.qrCodeVersion'); + } +} + +if (! function_exists('smarty_svg_error')) { + function smarty_svg_error($text) + { + return ' + + ' . $text . ' +'; + } +} diff --git a/app/Language/ar/Common.php b/app/Language/ar/Common.php index 3b0a2dd..ae4b317 100644 --- a/app/Language/ar/Common.php +++ b/app/Language/ar/Common.php @@ -1,6 +1,7 @@ 'لقد تم تعطيل جافا سكربت في المتصفح او ان المتصفح لا يدعم جافاسكربت وبالتالي لن يعمل التطبيق بالشكل الامثل ، فعل جافا سكربت او استخدم متصفح يدعم جافا سكربت.', 'dashboardTitle' => 'صفحة المعلومات', 'dashboardLnk' => 'صفحة المعلومات', 'accountSettingsLnk' => 'اعدادات الحساب', diff --git a/app/Language/ar/Url.php b/app/Language/ar/Url.php index 08ac704..a888706 100644 --- a/app/Language/ar/Url.php +++ b/app/Language/ar/Url.php @@ -21,6 +21,7 @@ 'AddNewUrlSubmitbtn' => 'انشاء الرابط الذكي', 'UpdateUrlSubmitbtn' => 'تحديث الرابط', 'UpdateUrlCancelbtn' => 'الغاء', + 'UpdateUrlConditionsTooltip' => 'تحديث شروط اعادة التوجية', 'GeographicalLocation' => 'الموقع الجغرافي', 'SelectCountry' => 'إختر الدولة', @@ -50,10 +51,13 @@ 'urlsListEntriesPerPage' => 'عدد النتائج في الصفحة:', 'urlsListErrorAjaxError' => 'حدث خطأ أثناء تحميل البيانات. حاول مرة اخرى.', 'urlListTags' => 'هاشتاجات الرابط', + 'urlListTagsNoTags' => 'لا يوجد هاشتاجات للرابط', 'UrlHitsNo' => 'عدد الزيارات', 'UrlId' => 'رقم', 'UrlTestUrl' => 'اختبار الرابط', 'UrlOwner' => 'مالك الرابط', + 'UrlCreateDate' => 'تاريخ انشاء الرابط', + 'UrlUpdateDate' => 'تاريخ اخر تحديث للرابط', 'urlNotFoundShort' => '404 رابط غير موجود', 'urlNotFoundLong' => 'أووبس ، الرابط الذي تحاول الوصول له غير موجود ربما تم حذفه او تعديله', @@ -64,4 +68,17 @@ 'urlIdentifieralreadyExists' => 'الرابط المختصر "{0}" موجود مسبقا في قاعدة البيانات ، اختر رابطا مختصرا اخرا', 'urlIdentifierPatternError' => 'عفوا! معرف الرابط المختصر الذي أدخلته غير صالح. يرجى التأكد من أنه أبجدي (انجليزي فقط) او رقمي ، ييدا بحرف او رقم يمكن ان يحتوي فقط على _ او - يجب أن يتكون أيضًا بين 1 و 50 خانة فقط. ', 'urlIdentifierNotAllowed' => 'المعرف المخصر المختصر غير مسموح به. قد يكون موجودًا بالفعل كمسار أو يكون كلمة محجوزة.', + + 'urlInfoTitle' => 'صفحة الرابط', + 'urlInfoRecdirectCondition' => 'شرط اعادة التوجيه', + 'urlInfoNoRecdirectCondition' => 'لم يتم تعريف شروط اعادة توجيه', + 'urlInfoLast25Hits' => 'اخر 25 زيارة للرابط', + 'urlInfoSeeAllHits' => 'استعراض جميع الزيارات', + 'urlInfoVisitDate' => 'التاريخ', + 'urlInfoVisitorIP' => 'عنوان الاي بي', + 'urlInfoVisitorCountry' => 'الدولة', + 'urlInfoVisitorDevice' => 'الجهاز', + 'urlInfoVisitorUserAgent' => 'الوكيل', + 'urlInfoFinalTarget' => 'الرابط النهائي', + 'urlInfoNoHitsYet' => 'لا يوجد زيارات للرابط حتى الان!', ]; diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index df2bf83..f3028a6 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -1,6 +1,7 @@ 'JavaScript is disabled in your browser, impacting the application performance. Please enable JavaScript or switch to a JavaScript-supported browser for optimal functionality.', 'dashboardTitle' => 'Dashboard', 'dashboardLnk' => 'Dashboard', 'accountSettingsLnk' => 'Account Settings', diff --git a/app/Language/en/Url.php b/app/Language/en/Url.php index b9a3d26..4dde5d1 100644 --- a/app/Language/en/Url.php +++ b/app/Language/en/Url.php @@ -21,6 +21,7 @@ 'AddNewUrlSubmitbtn' => 'Create Smarty Link', 'UpdateUrlSubmitbtn' => 'Update URL', 'UpdateUrlCancelbtn' => 'Cancel Update', + 'UpdateUrlConditionsTooltip' => 'Update redirect conditions', 'GeographicalLocation' => 'Geographical Location', 'SelectCountry' => 'Select Country', @@ -50,10 +51,13 @@ 'urlsListEntriesPerPage' => 'Entries per page:', 'urlsListErrorAjaxError' => 'An error occurred while loading data. Please try again.', 'urlListTags' => 'URL Tags', + 'urlListTagsNoTags' => 'No Tags for this URL', 'UrlHitsNo' => 'Hits No.', 'UrlId' => 'Url No.', 'UrlTestUrl' => 'Test Url', 'UrlOwner' => 'Url Owner', + 'UrlCreateDate' => 'URL create date', + 'UrlUpdateDate' => 'URL last update date', 'urlNotFoundShort' => '404 URL Not Found', 'urlNotFoundLong' => 'Oops, the link you are trying to access is not found. It may have been deleted or modified.', @@ -64,4 +68,17 @@ 'urlIdentifieralreadyExists' => 'Masked URL "{0}" Already Exists on database, choose another one', 'urlIdentifierPatternError' => 'Oops! The URL identifier you entered is not valid. Please make sure it is alphanumeric and should starts with alphanumeric and may contains only underscores _ and hyphens - It must also be between 1 and 50 characters long.', 'urlIdentifierNotAllowed' => 'The URL identifier (Masked URL) is not allowed. It may already exist as a route or be a reserved word.', + + 'urlInfoTitle' => 'URL Page', + 'urlInfoRecdirectCondition' => 'Redirection condition', + 'urlInfoNoRecdirectCondition' => 'No redirect conditions defined', + 'urlInfoLast25Hits' => 'URl Last 25 visits', + 'urlInfoSeeAllHits' => 'Show all visits', + 'urlInfoVisitDate' => 'Date', + 'urlInfoVisitorIP' => 'IP', + 'urlInfoVisitorCountry' => 'Country', + 'urlInfoVisitorDevice' => 'Device', + 'urlInfoVisitorUserAgent' => 'User-Agent', + 'urlInfoFinalTarget' => 'Final Target', + 'urlInfoNoHitsYet' => 'No visits for the URL yet!', ]; diff --git a/app/Models/UrlHitsModel.php b/app/Models/UrlHitsModel.php index 8b2d116..e103513 100644 --- a/app/Models/UrlHitsModel.php +++ b/app/Models/UrlHitsModel.php @@ -51,4 +51,19 @@ protected function initialize(): void $this->table = $this->dbtables['urlhits']; } + + /** + * get the last 25 hits for URL + * + * @return mixed + */ + public function getLast25Hits($UrlId) + { + // Using Query Builder to retrieve the last 25 rows + return $this->where('urlhit_urlid', $UrlId) + ->orderBy('urlhit_id', 'DESC') + ->limit(25) + ->get() + ->getResult(); + } } diff --git a/app/Views/basic/layout.php b/app/Views/basic/layout.php index 7db970c..0e4fe73 100644 --- a/app/Views/basic/layout.php +++ b/app/Views/basic/layout.php @@ -246,6 +246,16 @@ class="nav-link ">
+ + + renderSection('main') ?> diff --git a/app/Views/basic/url/404.php b/app/Views/basic/url/404.php index 374008e..a303cd0 100644 --- a/app/Views/basic/url/404.php +++ b/app/Views/basic/url/404.php @@ -52,8 +52,10 @@
-

+

+

+
@@ -65,6 +67,7 @@

+

¯\_(ツ)_/¯