| Current Path : /proc/1908984/root/proc/2603263/cwd/ |
| Current File : //proc/1908984/root/proc/2603263/cwd/NRFramework.tar |
File.php 0000644 00000024666 15235314576 0006165 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined( '_JEXEC' ) or die( 'Restricted access' );
use NRFramework\Mimes;
use Joomla\Filesystem\File as JoomlaFile;
use Joomla\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
class File
{
/**
* Upload file
*
* @param array $file The request file as posted by form
* @param string $upload_folder The upload folder where the file must be uploaded
* @param string $allowed_file_types A comma separated list of allowed file types like: .jpg, .gif, .png
* @param bool $allow_unsafe Allow the upload of unsafe files. See JFilterInput::isSafeFile() method.
* @param bool $random_prefix If is set to true, the filename will get a random unique prefix
* @param bool $random_suffix If is set to true, the filename will get a random unique suffix
*
* @return mixed String on success, Null on failure
*/
public static function upload($file, $upload_folder = null, $allowed_file_types = [], $allow_unsafe = false, $random_prefix = null, $random_suffix = false)
{
// Make sure we have a valid file array
if (!isset($file['name']) || !isset($file['tmp_name']))
{
throw new \Exception(Text::sprintf('NR_UPLOAD_ERROR_CANNOT_UPLOAD_FILE', $file['name']));
}
// Check file type
self::checkMimeOrDie($allowed_file_types, $file);
/**
* Try transiterating the file name using the native php function
*
* If the given filename is non-latin, then all characters will be removed from the filename via makeSafe and thus
* we wont be able to upload the file.
*
* @see https://github.com/joomla/joomla-cms/pull/27974
*/
if (!defined('t_isJ5') && function_exists('transliterator_transliterate') && function_exists('iconv'))
{
// Using iconv to ignore characters that can't be transliterated
$file['name'] = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", transliterator_transliterate('Any-Latin; Latin-ASCII;', $file['name']));
}
// Sanitize filename
$filename = JoomlaFile::makeSafe($file['name']);
if (!is_null($random_prefix))
{
$filename = uniqid($random_prefix) . '_' . $filename;
}
if (is_bool($random_suffix) && $random_suffix === true)
{
$file_data = File::pathinfo($filename);
$filename = $file_data['filename'] . '_' . uniqid($random_suffix) . '.' . $file_data['extension'];
}
$filename = str_replace(' ', '_', $filename);
// Setup the full file name
$upload_folder = is_null($upload_folder) ? self::getTempFolder() : $upload_folder;
$destination_file = implode(DIRECTORY_SEPARATOR, [$upload_folder, $filename]);
// If file exists, rename to copy_X
self::uniquefy($destination_file);
$destination_file = Path::clean($destination_file);
if (!JoomlaFile::upload($file['tmp_name'], $destination_file, false, $allow_unsafe))
{
throw new \Exception(Text::sprintf('NR_UPLOAD_ERROR_CANNOT_UPLOAD_FILE', $file['name']));
}
return $destination_file;
}
/**
* Moves a file from one directory to another. Destination directories will be created if they are not exist.
*
* @param string $source_file The source file path
* @param string $destination_file The destination file path
* @param bool $replace_existing Replace same files names, otherwise create a copy in the format copy_X
*
* @return mixed String on success
*/
public static function move($source_file, $destination_file, $replace_existing = false, $hash = false)
{
$destination_folder = dirname($destination_file);
// Create destination folders recursively
if (!self::createDirs($destination_folder))
{
throw new \Exception(Text::sprintf('NR_CANNOT_CREATE_FOLDER', $destination_folder));
}
// Don't replace files with the same name. Instead, append copy_x to this one.
if (!$replace_existing)
{
self::uniquefy($destination_file, $hash);
}
// Move file to the destination folder
if (!JoomlaFile::move($source_file, $destination_file))
{
throw new \Exception(Text::sprintf('NR_CANNOT_MOVE_FILE', $destination_file));
}
return Path::clean($destination_file);
}
/**
* Copies a file from one directory to another.
*
* @param string $source_file The source file path
* @param string $destination_file The destination file path
* @param bool $replace_existing Replace same files names, otherwise create a copy in the format copy_X
* @param bool $hash Whether to md5 hash the filename
*
* @return mixed String on success
*/
public static function copy($source_file, $destination_file, $replace_existing = false, $hash = false)
{
$destination_folder = dirname($destination_file);
// Create destination folders recursively
if (!self::createDirs($destination_folder))
{
throw new \Exception(Text::sprintf('NR_CANNOT_CREATE_FOLDER', $destination_folder));
}
// Don't replace files with the same name. Instead, append copy_x to this one.
if (!$replace_existing)
{
self::uniquefy($destination_file, $hash);
}
// Copy file to the destination folder
if (!JoomlaFile::copy($source_file, $destination_file))
{
throw new \Exception(Text::sprintf('NR_CANNOT_MOVE_FILE', $destination_file));
}
return Path::clean($destination_file);
}
/**
* Reads (and checks) the temp Joomla folder
*
* @return string
*/
public static function getTempFolder()
{
$ds = DIRECTORY_SEPARATOR;
$tmpdir = Factory::getConfig()->get('tmp_path');
if (realpath($tmpdir) == $ds . 'tmp')
{
$tmpdir = JPATH_SITE . $ds . 'tmp';
}
elseif (!is_dir($tmpdir))
{
$tmpdir = JPATH_SITE . $ds . 'tmp';
}
return Path::clean(trim($tmpdir) . $ds);
}
/**
* Checks if the path exists. If not creates the folders as well as subfolders.
*
* @param string $path The folder path
* @param string $protect If set to true, each folder will be protected by disabling PHP engine and preventing folder browsing
*
* @return bool
*/
public static function createDirs($path, $protect = true)
{
if (!is_dir($path))
{
mkdir($path, 0755, true);
// New folder created. Let's protect it.
if ($protect)
{
self::writeHtaccessFile($path);
self::writeIndexHtmlFile($path);
}
}
// Make sure the folder is writable
return @is_writable($path);
}
/**
* Checks whether a file type is in an allowed list
*
* @param mixed $allowed_types Array or a comma separated list of allowed file extensions or mime types. Eg: .jpg, .png, applicaton/pdf
* @param string $file_object The uploaded file as appears in the $_FILES array
*
* @return bool
*/
public static function checkMimeOrDie($allowed_types, $file_object)
{
$file_path = $file_object['tmp_name'];
$file_name = isset($file_object['name']) ? $file_object['name'] : basename($file_path);
// Do we have a mime type detected?
if (!$mime_type = Mimes::detectFileType($file_path))
{
throw new \Exception(Text::sprintf('NR_UPLOAD_NO_MIME_TYPE', $file_name));
}
if (!Mimes::check($allowed_types, $mime_type))
{
throw new \Exception(Text::sprintf('NR_UPLOAD_INVALID_FILE_TYPE', $file_name, $mime_type, $allowed_types));
}
}
/**
* Add an .htaccess file to the folder in order to disable PHP engine entirely
*
* @param string $path The path where to write the file
*
* @return void
*/
public static function writeHtaccessFile($path)
{
$content = '
# Block direct PHP access
<Files *.php>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</Files>
';
JoomlaFile::write($path . '/.htaccess', $content);
}
/**
* Creates an empty index.html file to prevent directory listing
*
* @param string $path The path where to write the file
*
* @return void
*/
public static function writeIndexHtmlFile($path)
{
$content = '<!DOCTYPE html><title></title>';
JoomlaFile::write($path . '/index.html', $content);
}
/**
* Generates a unique filename in case the give name already exists by appending copy_X suffix to filename.
*
* @param string $path The path to the file.
* @param bool $hash MD5 hashes the file name.
*
* @return void
*/
public static function uniquefy(&$path, $hash = false)
{
$path_parts = self::pathinfo($path);
$dir = $path_parts['dirname'];
$ext = $path_parts['extension'];
$actual_name = $path_parts['filename'];
$original_name = $actual_name;
// md5 hash the file name
if ($hash)
{
$actual_name = md5($actual_name);
// Initialize again the path due to md5 hash
$path = $dir . '/' . $actual_name . '.' . $ext;
}
$i = 1;
while(file_exists($dir . '/' . $actual_name . '.' . $ext))
{
$actual_name = (string) $original_name . '_copy_' . $i;
// md5 hash the file name
if ($hash)
{
$actual_name = md5($actual_name);
}
$path = $dir . '/' . $actual_name . '.' . $ext;
$i++;
}
}
/**
* Returns information about a file path with multi-byte support
*
* @param string $path The path to be parsed.
*
* @return array
*/
public static function pathinfo($path)
{
// Store temporary the currenty locale
$currentLocale = setlocale(LC_ALL, 0);
setlocale(LC_ALL, 'C.UTF-8');
$pathinfo = pathinfo($path);
// Set back to previus value
setlocale(LC_ALL, $currentLocale);
return $pathinfo;
}
/**
* Force download of the exported file
*
* @return void
*/
public static function download($filename, $path = null)
{
$path = is_null($path) ? self::getTempFolder() : $path;
$filename = $path . '/' . $filename;
if (!is_file($filename))
{
throw new \Exception('Invalid filename ' . $filename);
}
error_reporting(0);
// Send the appropriate headers to force the download in the browser
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: public', false);
header('Pragma: public');
header('Content-Length: ' . @filesize($filename));
// Clear the output buffer and disable output buffering
ob_clean();
flush();
// Read exported file to buffer
readfile($filename);
// Don't leave any clues on the server. Delete the file.
JoomlaFile::delete($filename);
jexit();
}
} Document.php 0000644 00000001552 15235314576 0007051 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class Document
{
/**
* A cross Joomla compatible method to inject inline script as module
*
* @param string $script The inline script to load as module (defer)
*
* @return void
*/
static public function addInlineScriptDefer($script)
{
$doc = Factory::getApplication()->getDocument();
if (defined('nrJ4'))
{
// Joomla => 4
$doc->getWebAssetManager()->addInlineScript($script, [], ['type' => 'module']);
} else
{
// Joomla <= 3
$doc->addCustomTag('<script type="module">' . $script . '</script>');
}
}
} Visitor.php 0000644 00000004346 15235314576 0006736 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Factory;
class Visitor
{
/**
* The name of the cookie used to identify that a visitor is persistent.
*
* @var string
*/
private $persistent_cookie_name = 'tvp';
/**
* Represents the maximum age of the visitor's persistent cookie in seconds.
*
* Default value set to 1 year.
*
* @var int
*/
private $persistent_cookie_expire = 31536000;
/**
* The name of the cookie used to identify that a visitor is new.
*
* @var string
*/
private $session_cookie_name = 'tvs';
/**
* Represents the maximum age of the visitor's session cookie in seconds.
*
* Default value set to 20 minutes.
*
* @var int
*/
private $session_cookie_expire = 1200;
/**
* The Cookies instance.
*
* @var object
*/
private $cookies;
public function __construct()
{
$this->cookies = Factory::getApplication()->input->cookie;
}
/**
* Creates or updates cookies of the visitor.
*
* - It will only create & update the tvs (visitor session cookie) when the user is considered new.
* - It will always update the tvp (visitor persistent cookie).
*
* @return void
*/
public function createOrUpdateCookie()
{
if ($this->isNew())
{
// Update the session cookie
$this->cookies->set($this->session_cookie_name, 1, time() + $this->session_cookie_expire, '/', '', true);
}
// Update the persistent cookie
$this->cookies->set($this->persistent_cookie_name, 1, time() + $this->persistent_cookie_expire, '/', '', true);
}
/**
* Checks whether the user is considered new.
*
* A user is considered new when the following criteria are met:
*
* - visitor persistent and session cookies are not met
* OR
* - visitor session cookie is set
*
* @return bool
*/
public function isNew()
{
$tvp = $this->cookies->get($this->persistent_cookie_name);
$tvs = $this->cookies->get($this->session_cookie_name);
return (!$tvp && !$tvs) || $tvs;
}
} Factory.php 0000644 00000005614 15235314576 0006705 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use \NRFramework\WebClient;
use \NRFramework\CacheManager;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Uri\Uri;
defined('_JEXEC') or die;
/**
* Framework Factory Class
*
* Used to decouple the framework from it's dependencies and make unit testing easier.
*
* @todo Rename class to Container and make all methods static.
*/
class Factory
{
public function isFrontend()
{
return $this->getApplication()->isClient('site');
}
public static function getCondition($name)
{
return \NRFramework\Conditions\ConditionsHelper::getInstance()->getCondition($name);
}
public function getDbo()
{
return JoomlaFactory::getDbo();
}
public function getApplication()
{
$app = JoomlaFactory::getApplication();
// The 'forward_context' parameter is used to forward data from one page to another. This is rather useful in XHR requests.
// This is mainly used in Convert Forms to make the {article} Smart Tag work after form submission.
if ($context = $app->input->get('forward_context', '', 'raw'))
{
if (is_string($context))
{
try
{
$context = json_decode($context, true);
$app->input->set('forward_context', $context);
} catch (\Throwable $th)
{
}
}
}
return $app;
}
public function getCookie($cookie_name)
{
return JoomlaFactory::getApplication()->input->cookie->get($cookie_name, null, 'string');
}
public function getDocument()
{
return JoomlaFactory::getDocument();
}
public function getUser($id = null)
{
return \NRFramework\User::get($id);
}
public function getCache()
{
return CacheManager::getInstance(JoomlaFactory::getCache('tassos', ''));
}
public function getDate($date = 'now', $tz = null)
{
return JoomlaFactory::getDate($date, $tz);
}
public function getURI()
{
return Uri::getInstance();
}
public function getURL()
{
return Uri::getInstance()->toString();
}
public function getLanguage()
{
return JoomlaFactory::getLanguage();
}
public function getSession()
{
return JoomlaFactory::getSession();
}
public function getDevice()
{
return WebClient::getDeviceType();
}
public function getBrowser()
{
return WebClient::getBrowser();
}
public function getExecuter($php_code)
{
return new \NRFramework\Executer($php_code);
}
} DOMCrawler.php 0000644 00000017646 15235314576 0007245 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use Joomla\String\StringHelper;
use NRFramework\Cache;
defined('_JEXEC') or die;
class DOMCrawler
{
/**
* The content to craw
*
* @var string
*/
protected $content;
/**
* The nodes discovered by crawling
*
* @var object
*/
public $nodes;
/**
* Class constructor
*
* @param mixed $content The content to crawl. Defaults
*/
public function __construct($content = null)
{
if (is_null($content))
{
$content = \NRFramework\Functions::getBuffer();
}
$this->setContent($content);
}
/**
* Set content to crawl
*
* @param string $content The content to crawl. Defaults
* @return void
*/
public function setContent($content)
{
$this->content = $this->stringToUTF8($content);
}
/**
* Filter dom elements with a CSS Selector or XPath expression
*
* @param string $expression A CSS Selector or XPath expression
*
* @return void
*/
public function filter($expression)
{
// If empty content, return
if (empty($this->content))
{
return $this;
}
// If empty selector, return
if (empty($expression))
{
return $this;
}
if (!class_exists('DOMDocument') || !class_exists('DOMXPath'))
{
return $this;
}
// Cache check
$hash = md5($expression);
if (Cache::has($hash))
{
$this->nodes = Cache::get($hash, false);
return $this;
}
libxml_use_internal_errors(true);
$dom = new \DOMDocument;
$dom->loadHTML($this->content);
$finder = new \DOMXPath($dom);
// Check if we are writing our own XPath query
// example: =//h1[contains(@class, "faq-question")]
if (substr($expression, 0, 1) == '=')
{
$xpath = StringHelper::substr($expression, 1);
}
else
{
// Create the XPath via the provided selector
$xpath = $this->cssSelectorToXPath($expression);
}
$this->nodes = $finder->query($xpath);
// Speed up filtering by caching results
Cache::set($hash, $this->nodes);
return $this;
}
/**
* Returns the HTML of the first discovered node
*
* @param string $fallback The fallback text to return if no node is found
* @param boolean $inner If set to true, only the node's inner HTML will be returned.
* @param boolean $firstOnly If set to true, only the first node will be returned.
*
* @return string
*/
public function html($fallback = '', $inner = false, $firstOnly = true)
{
if (!$this->nodes || !$this->nodes->length)
{
return $fallback;
}
if ($firstOnly)
{
return $this->cleanText($this->getNodeHTML($this->nodes[0], $inner));
}
$result = [];
foreach ($this->nodes as $node)
{
$result[] = $this->cleanText($this->getNodeHTML($node, $inner));
}
return $result;
}
/**
* Returns the text of the 1st discovered node.
*
* @param string $fallback The fallback text to return if no node is found
* @param boolean $firstOnly If set to true, only the first node will be returned.
*
* @return string
*/
public function text($fallback = '', $firstOnly = true)
{
if (!$this->nodes || !$this->nodes->length)
{
return $fallback;
}
if ($firstOnly)
{
return $this->cleanText($this->nodes[0]->textContent);
}
$result = [];
foreach ($this->nodes as $node)
{
$result[] = $this->cleanText($node->textContent);
}
return $result;
}
/**
* Returns the attribute value of the 1st discovered node
*
* @param string $attribute_name The name of the attribute
* @param string $fallback The fallback text to return if no nodes found
* @param boolean $firstOnly If set to true, only the first node will be returned.
*
* @return string
*/
public function attr($attribute_name, $fallback = '', $firstOnly = true)
{
if (!$this->nodes || !$this->nodes->length)
{
return $fallback;
}
if ($firstOnly)
{
return $this->cleanText($this->nodes[0]->getAttribute($attribute_name));
}
$result = [];
foreach ($this->nodes as $node)
{
$result[] = $this->cleanText($node->getAttribute($attribute_name));
}
return $result;
}
/**
* Returns the total number of nodes found
*
* @param integer $fallback The fallback value number to return if no nodes found
*
* @return integer
*/
public function count($fallback = 0)
{
return $this->nodes && $this->nodes->length ? $this->nodes->length : $fallback;
}
/**
* Helper method to crawl page based on the value of a CSS Selector field.
*
* @param array $props Expected properties: selector, task, attr
*
* @return string
*/
public function readCSSSelectorField($props, $firstOnly = true)
{
$props = (array) $props;
$fallback = $firstOnly ? '' : [];
if (empty($props['selector']))
{
return $fallback;
}
$this->filter($props['selector']);
switch ($props['task'])
{
case 'html':
return $this->html($fallback, false, $firstOnly);
case 'innerhtml':
return $this->html($fallback, true, $firstOnly);
case 'attr':
return $this->attr($props['attr'], $fallback, $firstOnly);
case 'count':
return $this->count();
default:
return $this->text($fallback, $firstOnly);
}
}
/**
* Helper method to clean the text
*
* @param string $text The text to clean
*
* @return string
*/
private function cleanText($text)
{
return StringHelper::trim($text);
}
/**
* Transforms the CSS Selector to a valid XPath expression
*
* @param string $selector The CSS selector to transform
*
* @return string XPath expression
*/
private function cssSelectorToXPath($selector)
{
// explode() the given selectors and create a XPath syntax
$selectors = explode(' ', $selector);
$xpath = '';
foreach ($selectors as $selector)
{
// Check if the selector contains a class or ID
$explode_class = explode('.', $selector);
$explode_id = explode('#', $selector);
// Selector contains a class
if (count($explode_class) > 1)
{
$prefix = (isset($explode_class[0]) && !empty($explode_class[0])) ? $explode_class[0] : '*';
$xpath .= '//' . $prefix . '[';
// When we use a selector such as div.class1.class2 or .class1.class2
// we need to use all classes in the xpath and no the first one only
unset($explode_class[0]);
$total = count($explode_class);
$counter = 1;
$xpath_and_prefix = 'and';
foreach ($explode_class as $class)
{
$xpath .= ($counter != 1) ? $xpath_and_prefix : '';
$xpath .= ' contains(concat(" ", normalize-space(@class), " "), " ' . $class . ' ") ';
$counter++;
}
$xpath .= ']';
}
else if (count($explode_id) > 1) // Selector contains an ID
{
$prefix = (isset($explode_id[0]) && !empty($explode_id[0])) ? $explode_id[0] : '*';
$xpath .= './/' . $prefix . '[@id="' . $explode_id[1] . '"]';
}
else // No class or ID given
{
$xpath .= '//' . $selector;
}
}
return $xpath;
}
/**
* Convert a string to UTF8 encoding for non-latin languages
*
* @param string
*
* @return string
*/
private function stringToUTF8($string)
{
$string = iconv('UTF-8', 'UTF-8', $string);
$string = mb_encode_numericentity($string, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
return $string;
}
/**
* Helper method to return the outer or inner HTML of a node
*
* @param Node $node The node object
* @param boolean $inner Whether to return the outer or inner HTML
*
* @return string The HTML of the node
*/
private function getNodeHTML($node, $inner = true)
{
if ($inner)
{
$html = '';
foreach ($node->childNodes as $child)
{
$html .= $node->ownerDocument->saveHTML($child);
}
return $html;
}
return $node->ownerDocument->saveHTML($node);
}
} Updatesites.php 0000644 00000007422 15235314576 0007567 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Factory;
class Updatesites
{
/**
* Joomla Database Class
*
* @var object
*/
private $db;
/**
* The download key.
*
* @var string
*/
private $key;
/**
* Consturction method
*
* @param string $key Download Key
*/
public function __construct($key = null)
{
$this->db = Factory::getDBO();
$this->key = ($key) ? $key : $this->getDownloadKey();
}
/**
* Main method
*/
public function update()
{
$this->removeDuplicates();
$this->updateHttptoHttps();
$this->removeInstallerUpdateSite();
}
/**
* Reads the Download Key saved in the framework plugin parameters
*
* @return string The Download Key
*/
public function getDownloadKey()
{
$hash = 'nrframework_download_key';
$cache = Cache::read($hash);
if ($cache)
{
return $cache;
}
$query = $this->db->getQuery(true)
->select('e.params')
->from('#__extensions as e')
->where('e.element = ' . $this->db->quote('nrframework'));
$this->db->setQuery($query);
if (!$params = $this->db->loadResult())
{
return;
}
$params = json_decode($params);
if (!isset($params->key))
{
return;
}
return Cache::set($hash, trim($params->key));
}
/**
* Remove our Installer's update site left over from the database
*
* @return void
*/
private function removeInstallerUpdateSite()
{
$query = $this->db->getQuery(true)
->delete('#__update_sites')
->where($this->db->quoteName('name') . ' = ' . $this->db->quote('System - Novarain Installer'));
$this->db->setQuery($query);
$this->db->execute();
}
/**
* Update http to https
*
* @return void
*/
private function updateHttptoHttps()
{
$query = $this->db->getQuery(true)
->update('#__update_sites')
->set($this->db->quoteName('location') . ' = REPLACE('
. $this->db->quoteName('location') . ', '
. $this->db->quote('http://') . ', '
. $this->db->quote('https://')
. ')')
->where($this->db->quoteName('location') . ' LIKE ' . $this->db->quote('%tassos.gr%'));
$this->db->setQuery($query);
$this->db->execute();
}
/**
* Remove duplicate update sites created by upgrading from Free to Pro version
*
* @return void
*/
private function removeDuplicates()
{
$db = $this->db;
// Find duplicates first
$query = 'SELECT name, COUNT(*) c FROM #__update_sites where location like "%tassos.gr%" GROUP BY name HAVING c > 1';
$db->setQuery($query);
if (!$duplicates = $db->loadObjectList())
{
return;
}
// OK we have duplicates. Let's remove them.
foreach ($duplicates as $key => $duplicate)
{
// Get all IDs
$query = $db->getQuery(true)
->select('update_site_id')
->from('#__update_sites')
->where('name = ' . $db->quote($duplicate->name))
->order('update_site_id DESC');
$db->setQuery($query);
if (!$update_site_ids = $db->loadObjectList())
{
return;
}
// Skip the 1st index which represents the last created and valid.
unset($update_site_ids[0]);
foreach ($update_site_ids as $key => $update_site_id)
{
$id = $update_site_id->update_site_id;
$query->clear()
->delete('#__update_sites')
->where($db->quoteName('update_site_id') . ' = ' . (int) $id);
$db->setQuery($query);
$db->execute();
$query->clear()
->delete('#__update_sites_extensions')
->where($db->quoteName('update_site_id') . ' = ' . (int) $id);
$db->setQuery($query);
$db->execute();
}
}
}
} URL.php 0000644 00000006417 15235314576 0005742 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use NRFramework\Factory;
use Joomla\CMS\Uri\Uri;
class URL
{
/**
* The path.
*
* @var string
*/
private $path;
/**
* The Factory.
*
* @var Factory
*/
private $factory;
/**
* Class constructor
*/
public function __construct($path, $factory = null)
{
$this->path = trim($path ?? '');
$this->factory = $factory ? $factory : new Factory();
}
public function getInstance()
{
return clone Uri::getInstance($this->path);
}
public function getDomainName()
{
return strtolower(str_ireplace('www.', '', $this->getInstance()->getHost()));
}
public function isAbsolute()
{
return !is_null($this->getInstance()->getScheme());
}
public function isInternal()
{
if (!$this->path)
{
return false;
}
$host = $this->getInstance()->getHost();
if (is_null($host))
{
return true;
}
$siteHost = $this->factory->getURI()->getHost();
return preg_match('#' . preg_quote($siteHost, '#') . '#', $host) ? true : false;
}
/**
* Transform a relative path to absolute URL
*
* @return string
*/
public function toAbsolute()
{
if (empty($this->path))
{
return;
}
// Check if it's already absolute URL
if ($this->isAbsolute())
{
return $this->path;
}
$basePath = \parse_url(Uri::root());
$parse_path = $this->getInstance();
$parse_path->setScheme($basePath['scheme']);
$parse_path->setHost($basePath['host']);
$parse_path->setPath($basePath['path'] . $parse_path->getPath());
return $parse_path->toString();
}
/**
* CDNify a resource
*
* @param string $host The hostname of the CDN to be used
* @param string $scheme
*
* @return string
*/
public function cdnify($host, $scheme = 'https')
{
// Allow only internal URLs
if (!$this->isInternal())
{
return $this->path;
}
// Allow only resource files
$path = $this->getInstance()->getPath();
if (strpos($path, '.') === false)
{
return $this->path;
}
return $this->setHost($host, $scheme);
}
public function setHost($domain, $scheme = 'https')
{
if (empty($this->path))
{
return;
}
$url_new = $this->getInstance();
$url_new->setScheme($scheme);
$url_new->setHost($domain);
// Path should always start with a slash
if ($url_new->getPath())
{
$url_new->setPath('/' . ltrim($url_new->getPath(), '/'));
}
$result = $url_new->toString();
if ($scheme == '//')
{
$result = str_replace('://', '', $result);
}
return $result;
}
} CacheManager.php 0000644 00000005471 15235314576 0007575 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die;
/**
* Cache Manager
*
* Singleton
*/
class CacheManager
{
/**
* 'static' cache array
* @var array
*/
protected $cache = [];
/**
* Cache mechanism object
* @var object
*/
protected $cache_mechanism = null;
/**
* Construct
*/
protected function __construct($cache_mechanism)
{
$this->cache_mechanism = $cache_mechanism;
}
static public function getInstance($cache_mechanism)
{
static $instance = null;
if ($instance === null)
{
$instance = new CacheManager($cache_mechanism);
}
return $instance;
}
/**
* Check if a hash already exists in memory
*
* @param string $hash The hash string
*
* @return boolean
*/
public function has($hash)
{
return isset($this->cache[$hash]);
}
/**
* Returns a hash's value
*
* @param string $hash The hash string
* @param string $clone Why the hell we clone objects here?
*
* @return mixed False on error, Object on success
*/
public function get($hash, $clone = true)
{
if (!$this->has($hash))
{
return false;
}
return is_object($this->cache[$hash]) && $clone ? clone $this->cache[$hash] : $this->cache[$hash];
}
/**
* Sets a hash value
*
* @param string $hash The hash string
* @param mixed $data Can be string or object
*
* @return mixed
*/
public function set($hash, $data)
{
$this->cache[$hash] = $data;
return $data;
}
/**
* Reads a hash value from memory or file
*
* @param string $hash The hash string
* @param boolean $force If true, the filesystem will be used as well on the /cache/ folder
*
* @return mixed The hash object value
*/
public function read($hash, $force = false)
{
if ($this->has($hash))
{
return $this->get($hash);
}
if ($force)
{
$this->cache_mechanism->setCaching(true);
}
return $this->cache_mechanism->get($hash);
}
/**
* Writes hash value in cache folder
*
* @param string $hash The hash string
* @param mixed $data Can be string or object
* @param integer $ttl Expiration duration in minutes. Default 1440 minutes = 1 day.
*
* @return mixed The hash object value
*/
public function write($hash, $data, $ttl = 1440)
{
if ($ttl > 0)
{
$this->cache_mechanism->setLifeTime($ttl);
}
$this->cache_mechanism->setCaching(true);
$this->cache_mechanism->store($data, $hash);
$this->set($hash, $data);
return $data;
}
} Continents.php 0000644 00000002537 15235314576 0007423 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Language\Text;
/**
* Helper class to work with continent names/codes
*/
class Continents
{
/**
* Return a continent code from it's name
*
* @param string $cont
* @return string|void
*/
public static function getCode($cont)
{
$cont = \ucwords(strtolower($cont));
foreach (self::getContinentsList() as $key => $value)
{
if (strpos($value, $cont) !== false)
{
return $key;
}
}
return null;
}
/**
* Returns a list of continents
*
* @return array
*/
public static function getContinentsList()
{
return [
'AF' => Text::_('NR_CONTINENT_AF'),
'AS' => Text::_('NR_CONTINENT_AS'),
'EU' => Text::_('NR_CONTINENT_EU'),
'NA' => Text::_('NR_CONTINENT_NA'),
'SA' => Text::_('NR_CONTINENT_SA'),
'OC' => Text::_('NR_CONTINENT_OC'),
'AN' => Text::_('NR_CONTINENT_AN'),
];
}
} Mimes.php 0000644 00000035666 15235314576 0006362 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
* @credits https://github.com/codeigniter4/CodeIgniter4/blob/develop/app/Config/Mimes.php
*/
namespace NRFramework;
// No direct access
defined('_JEXEC') or die;
class Mimes
{
/**
* Map of extensions to mime types.
*
* @var array
*/
public static $mimes = [
'hqx' => [
'application/mac-binhex40',
'application/mac-binhex',
'application/x-binhex40',
'application/x-mac-binhex40',
],
'cpt' => 'application/mac-compactpro',
'csv' => [
'text/csv',
'text/x-comma-separated-values',
'text/comma-separated-values',
'application/vnd.ms-excel',
'application/x-csv',
'text/x-csv',
'application/csv',
'application/excel',
'application/vnd.msexcel',
'text/plain',
],
'bin' => [
'application/macbinary',
'application/mac-binary',
'application/octet-stream',
'application/x-binary',
'application/x-macbinary',
],
'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'exe' => [
'application/octet-stream',
'application/x-msdownload',
],
'class' => 'application/octet-stream',
'psd' => [
'application/x-photoshop',
'image/vnd.adobe.photoshop',
],
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => [
'application/pdf',
'application/force-download',
'application/x-download',
],
'ai' => [
'application/pdf',
'application/postscript',
],
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => [
'application/vnd.ms-excel',
'application/msexcel',
'application/x-msexcel',
'application/x-ms-excel',
'application/x-excel',
'application/x-dos_ms_excel',
'application/xls',
'application/x-xls',
'application/excel',
'application/download',
'application/vnd.ms-office',
'application/msword',
],
'ppt' => [
'application/vnd.ms-powerpoint',
'application/powerpoint',
'application/vnd.ms-office',
'application/msword',
],
'pptx' => [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/x-zip',
'application/zip',
],
'wbxml' => 'application/wbxml',
'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip',
'php' => [
'application/x-php',
'application/x-httpd-php',
'application/php',
'text/php',
'text/x-php',
'application/x-httpd-php-source',
],
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'js' => [
'application/x-javascript',
'text/plain',
],
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => [
'application/x-tar',
'application/x-gzip-compressed',
],
'z' => 'application/x-compress',
'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml',
'zip' => [
'application/x-zip',
'application/zip',
'application/x-zip-compressed',
'application/s-compressed',
'multipart/x-zip',
],
'rar' => [
'application/vnd.rar',
'application/x-rar',
'application/rar',
'application/x-rar-compressed',
],
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg',
'mp3' => [
'audio/mpeg',
'audio/mpg',
'audio/mpeg3',
'audio/mp3',
],
'aif' => [
'audio/x-aiff',
'audio/aiff',
],
'aiff' => [
'audio/x-aiff',
'audio/aiff',
],
'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => [
'audio/x-wav',
'audio/wave',
'audio/wav',
],
'bmp' => [
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
'image/x-xbitmap',
'image/x-win-bitmap',
'image/x-windows-bmp',
'image/ms-bmp',
'image/x-ms-bmp',
'application/bmp',
'application/x-bmp',
'application/x-win-bitmap',
],
'gif' => 'image/gif',
'jpg' => [
'image/jpeg',
'image/pjpeg',
],
'jpeg' => [
'image/jpeg',
'image/pjpeg',
],
'jpe' => [
'image/jpeg',
'image/pjpeg',
],
'jp2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'j2k' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpf' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpg2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpx' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpm' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'mj2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'mjp2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'png' => [
'image/png',
'image/x-png',
],
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'css' => [
'text/css',
'text/plain',
],
'html' => [
'text/html',
'text/plain',
],
'htm' => [
'text/html',
'text/plain',
],
'shtml' => [
'text/html',
'text/plain',
],
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => [
'text/plain',
'text/x-log',
],
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => [
'application/xml',
'text/xml',
'text/plain',
],
'xsl' => [
'application/xml',
'text/xsl',
'text/xml',
],
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => [
'video/x-msvideo',
'video/msvideo',
'video/avi',
'application/x-troff-msvideo',
],
'movie' => 'video/x-sgi-movie',
'doc' => [
'application/msword',
'application/vnd.ms-office',
],
'docx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
'application/x-zip',
],
'dot' => [
'application/msword',
'application/vnd.ms-office',
],
'dotx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
],
'xlsx' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/zip',
'application/vnd.ms-excel',
'application/msword',
'application/x-zip',
],
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'word' => [
'application/msword',
'application/octet-stream',
],
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => [
'application/json',
'text/json',
],
'pem' => [
'application/x-x509-user-cert',
'application/x-pem-file',
'application/octet-stream',
],
'p10' => [
'application/x-pkcs10',
'application/pkcs10',
],
'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature',
'p7c' => [
'application/pkcs7-mime',
'application/x-pkcs7-mime',
],
'p7m' => [
'application/pkcs7-mime',
'application/x-pkcs7-mime',
],
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'crt' => [
'application/x-x509-ca-cert',
'application/x-x509-user-cert',
'application/pkix-cert',
],
'crl' => [
'application/pkix-crl',
'application/pkcs-crl',
],
'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7',
'cer' => [
'application/pkix-cert',
'application/x-x509-ca-cert',
],
'3g2' => 'video/3gpp2',
'3gp' => [
'video/3gp',
'video/3gpp',
],
'mp4' => 'video/mp4',
'm4a' => 'audio/x-m4a',
'f4v' => [
'video/mp4',
'video/x-f4v',
],
'flv' => 'video/x-flv',
'webm' => 'video/webm',
'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain',
'xspf' => 'application/xspf+xml',
'vlc' => 'application/videolan',
'wmv' => [
'video/x-ms-wmv',
'video/x-ms-asf',
],
'au' => 'audio/x-au',
'ac3' => 'audio/ac3',
'flac' => [
'audio/x-flac',
'audio/flac'
],
'ogg' => [
'audio/ogg',
'video/ogg',
'application/ogg',
],
'kmz' => [
'application/vnd.google-earth.kmz',
'application/zip',
'application/x-zip',
],
'kml' => [
'application/vnd.google-earth.kml+xml',
'application/xml',
'text/xml',
],
'ics' => 'text/calendar',
'ical' => 'text/calendar',
'zsh' => 'text/x-scriptzsh',
'7z' => [
'application/x-compressed',
'application/x-zip-compressed',
'application/x-7z-compressed',
'application/zip',
'multipart/x-zip',
],
'7zip' => [
'application/x-compressed',
'application/x-zip-compressed',
'application/x-7z-compressed',
'application/zip',
'multipart/x-zip',
],
'cdr' => [
'application/cdr',
'application/coreldraw',
'application/x-cdr',
'application/x-coreldraw',
'image/cdr',
'image/x-cdr',
'zz-application/zz-winassoc-cdr',
],
'wma' => [
'audio/x-ms-wma',
'video/x-ms-asf',
],
'jar' => [
'application/java-archive',
'application/x-java-application',
'application/x-jar',
'application/x-compressed',
],
'svg' => [
'image/svg+xml',
'image/svg',
'application/xml',
'text/xml',
],
'vcf' => 'text/x-vcard',
'srt' => [
'text/srt',
'text/plain',
],
'vtt' => [
'text/vtt',
'text/plain',
],
'ico' => [
'image/x-icon',
'image/x-ico',
'image/vnd.microsoft.icon',
],
'stl' => [
'application/sla',
'application/vnd.ms-pki.stl',
'application/x-navistyle',
],
];
/**
* Attempts to determine the best mime type for the given file extension.
*
* @param string $extension
*
* @return string|null The mime type found, or none if unable to determine.
*/
public static function getTypesFromExtension($extension)
{
$extension = trim(strtolower($extension), '. ');
if (!array_key_exists($extension, static::$mimes))
{
return null;
}
return (array) static::$mimes[$extension];
}
/**
* Attempts to determine the best file extension for a given mime type.
*
* @param string $type
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
*
* @return string|null The extension determined, or null if unable to match.
*/
public static function guessExtensionFromType($type, $proposedExtension = null)
{
$type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension));
if ($proposedExtension !== '')
{
if (array_key_exists($proposedExtension, static::$mimes) && in_array($type, is_string(static::$mimes[$proposedExtension]) ? [static::$mimes[$proposedExtension]] : static::$mimes[$proposedExtension], true))
{
// The detected mime type matches with the proposed extension.
return $proposedExtension;
}
// An extension was proposed, but the media type does not match the mime type list.
return null;
}
// Reverse check the mime type list if no extension was proposed.
// This search is order sensitive!
foreach (static::$mimes as $ext => $types)
{
if ((is_string($types) && $types === $type) || (is_array($types) && in_array($type, $types, true)))
{
return $ext;
}
}
return null;
}
/**
* Test whether the given mime type is in the allowed file types.
*
* @param mixed $allowed_types Can be a list of comma separated types or an array of types. Types can be either an extension (.jpg) or a mime type (application/zip)
* @param string $mime The mime type to check
*
* @return mixed Null on failure, true on success
*/
public static function check($allowed_types, $detected_mime)
{
if (!$allowed_types || !$detected_mime)
{
return false;
}
$allowed_types = self::toSafeArray($allowed_types);
foreach ($allowed_types as $allowed_type)
{
// Check whether we have a mime type or a file extension. A Mime type is supposed to have a forward slash character.
// If we have a file extension (.jpg, .zip), convert it to a Mime type.
$allowed_mime_types = strpos($allowed_type, '/') === false ? self::getTypesFromExtension($allowed_type) : $allowed_type;
if (self::typeIsInTypes($detected_mime, $allowed_mime_types))
{
return true;
}
}
}
/**
* Test whether the given detected mime type is in allowed mime types
*
* @param string $detected_type The mime type to check Eg: application/zip
* @param array $allowed_types A list of allowed mime types Eg: ['application/zip', 'images/jpg']
*
* @return bool True on success
*/
public static function typeIsInTypes($detected_type, $allowed_types)
{
if (!$detected_type || !$allowed_types)
{
return;
}
$allowed_types = self::toSafeArray($allowed_types);
$detected_type = strtolower($detected_type);
foreach ($allowed_types as $allowed_type)
{
// Special case: Allow to use wildcard in mime types like: image/* - This requires to convert the asterisk character to regex pattern.
$allowed_type = str_replace('*', '.*', $allowed_type);
if (preg_match('#' . $allowed_type . '#', $detected_type))
{
return true;
}
}
}
/**
* Detect the filename's Mime type
*
* @param string $file The path to the file to be checked
*
* @return mixed the mime type detected false on error
*/
public static function detectFileType($file)
{
// If we can't detect anything mime is false
$mime = false;
try
{
if (function_exists('mime_content_type'))
{
$mime = mime_content_type($file);
}
elseif (function_exists('finfo_open'))
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file);
finfo_close($finfo);
}
}
catch (\Exception $e)
{
}
return $mime;
}
private static function toSafeArray($subject)
{
if (!is_array($subject))
{
$subject = explode(',', $subject);
}
$subject = array_map('trim', $subject);
$subject = array_map('strtolower', $subject);
$subject = array_unique($subject);
$subject = array_filter($subject);
return $subject;
}
} VisitorToken.php 0000644 00000004356 15235314576 0007740 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Factory;
use Joomla\CMS\Crypt\Crypt;
class VisitorToken
{
/**
* Class instance
*
* @var object
*/
private static $instance;
/**
* Cookie Name
*
* @var string
*/
private $cookieName = "nrid";
/**
* Represents the maximum age of the visitor's cookie in seconds.
*
* @var Integer
*/
private $expire = 90000000;
/**
* Cookies Object
*
* @var object
*/
private $cookies;
/**
* Class constructor
*/
private function __construct()
{
$this->cookies = Factory::getApplication()->input->cookie;
$token = $this->cookies->get($this->cookieName, null);
if ($token === null)
{
$this->store($this->create());
}
}
/**
* Returns class instance
*
* @return object
*/
public static function getInstance()
{
if (is_null(self::$instance))
{
self::$instance = new self();
}
return self::$instance;
}
/**
* Get a visitor's unique token id, if a token isn't set yet one will be generated.
*
* @param boolean $forceNew If true, force a new token to be created
*
* @return string The session token
*/
public function get($forceNew = false)
{
return $this->cookies->get($this->cookieName);
}
/**
* Create a token-string
*
* @param integer $length Length of string
*
* @return string Generated token
*/
private function create($length = 8)
{
return bin2hex(Crypt::genRandomBytes($length));
}
/**
* Saves the cookie to the visitor's browser
*
* @param string $value Cookie Value
*
* @return void
*/
private function store($value)
{
$this->cookies->set($this->cookieName, $value, time() + $this->expire, '/', '', true);
}
} HTML.php 0000644 00000032735 15235314576 0006046 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
// No direct access
defined('_JEXEC') or die;
use NRFramework\Cache;
use NRFramework\Functions;
use NRFramework\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Session\Session;
class HTML
{
/**
* Display field help text as tooltip in Joomla 4
*
* @return void
*/
public static function fixFieldTooltips()
{
// Run once
static $run;
if ($run)
{
return;
}
$run = true;
HTMLHelper::_('bootstrap.popover');
HTMLHelper::_('jquery.framework');
$doc = Factory::getDocument();
$doc->addStyleDeclaration('
.form-text, .form-control-feedback {
display:none;
}
.tooltip .arrow:before {
border-top-color:#444;
border-bottom-color:#444;
}
.tooltip-inner {
text-align: left;
background-color: #444;
padding: 7px 9px;
max-width:300px;
}
');
$doc->addScriptDeclaration('
document.addEventListener("DOMContentLoaded", function() {
initPopover();
document.addEventListener("joomla:updated", initPopover);
function initPopover(event) {
var target = event && event.target ? event.target : document;
var fields = target.querySelectorAll(".control-group");
fields.forEach(function(field) {
var desc = field.querySelector(".form-text");
if (desc) {
var label = field.querySelector("label");
if (label) {
label.classList.add("tTooltip");
label.setAttribute("title", desc.innerHTML);
}
}
});
jQuery(target).find(".tTooltip").tooltip({
placement: "top",
html: true,
delay: {
show: 200
}
});
}
});
');
}
/**
* Renders the HTML layout
*
* @param string $layout The HTML class of the layout.
* @param array $options A list of attributes passed to the layout
*
* @return string
*/
public static function render($layout, $options = [])
{
if (!$layout)
{
return;
}
$class = '\NRFramework\HTML\\' . $layout;
// ensure class exists
if (!class_exists($class))
{
return;
}
return (new $class($options))->render();
}
/**
* Renders Pro Button
*
* @param string $feature_label The text that will be used as the modal popup feature
*
* @return void
*/
public static function renderProButton($feature_label = null)
{
include_once JPATH_PLUGINS . '/system/nrframework/fields/pro.php';
$field = new \JFormFieldNR_PRO;
$element = new \SimpleXMLElement('
<field name="pro" type="nr_pro"
label="' . $feature_label . '"
/>');
$field->setup($element, null);
echo $field->__get('input');
}
/**
* Renders a modal that will be shown on Pro only features
*
* @param string $extension_name
*
* @return void
*/
public static function renderProOnlyModal($extension = null)
{
$hash = 'proOnlyModal';
// Render modal once
if (Cache::get($hash))
{
return;
}
$options = [
'extension_name' => is_null($extension) ? Extension::getExtensionNameByRequest(true) : Extension::getExtensionName($extension),
'upgrade_url' => Extension::getTassosExtensionUpgradeURL($extension)
];
$html = LayoutHelper::render('proonlymodal', $options, dirname(__DIR__) . '/layouts');
echo HTMLHelper::_('bootstrap.renderModal', 'proOnlyModal', ['backdrop' => 'static'], $html);
Cache::set($hash, true);
}
public static function smartTagsBox($options = array())
{
HTMLHelper::_('jquery.framework');
include_once JPATH_PLUGINS . '/system/nrframework/fields/smarttagsbox.php';
$field = new \JFormFieldSmartTagsBox;
$element = new \SimpleXMLElement('<field name="pro" type="SmartTagsBox"/>');
$field->setup($element, null);
return $field->__get('input');
}
/**
* Construct the HTML for the input field in a tree
* Logic from administrator\components\com_modules\views\module\tmpl\edit_assignment.php
*/
public static function treeselect(&$options, $name, $value, $id, $size = 300, $simple = 0, $class = '')
{
Functions::loadLanguage('com_menus', JPATH_ADMINISTRATOR);
Functions::loadLanguage('com_modules', JPATH_ADMINISTRATOR);
if (empty($options))
{
return '<fieldset class="radio">' . Text::_('NR_NO_ITEMS_FOUND') . '</fieldset>';
}
if (!is_array($value))
{
$value = explode(',', $value);
}
$count = 0;
if ($options != -1)
{
foreach ($options as $option)
{
$count++;
if (isset($option->links))
{
$count += count($option->links);
}
}
}
if ($options == -1)
{
if (is_array($value))
{
$value = implode(',', $value);
}
if (!$value)
{
$input = '<textarea name="' . $name . '" id="' . $id . '" cols="40" rows="5">' . $value . '</textarea>';
}
else
{
$input = '<input type="text" name="' . $name . '" id="' . $id . '" value="' . $value . '" size="60">';
}
return '<fieldset class="radio"><label for="' . $id . '">' . Text::_('NR_ITEM_IDS') . ':</label>' . $input . '</fieldset>';
}
if ($simple)
{
$attr = 'style="width: ' . $size . 'px" multiple="multiple"';
if (!empty($class))
{
$attr .= ' class="' . $class . '"';
}
$html = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id);
return $html;
}
HTMLHelper::script('plg_system_nrframework/treeselect.js', ['relative' => true, 'version' => true]);
HTMLHelper::stylesheet('plg_system_nrframework/treeselect.css', ['relative' => true, 'version' => true]);
$html = array();
$html[] = '<div class="nr_treeselect" id="' . $id . '">';
$html[] = '
<div class="form-inline nr_treeselect-controls">
<span class="nr_treeselect_control">' . Text::_('JSELECT') . ':
<a class="nr_treeselect-checkall" href="javascript:;">' . Text::_('JALL') . '</a>,
<a class="nr_treeselect-uncheckall" href="javascript:;">' . Text::_('JNONE') . '</a>,
<a class="nr_treeselect-toggleall" href="javascript:;">' . Text::_('NR_TOGGLE') . '</a>
</span>
<span class="nr_treeselect_control">' . Text::_('NR_EXPAND') . ':
<a class="nr_treeselect-expandall" href="javascript:;">' . Text::_('JALL') . '</a>,
<a class="nr_treeselect-collapseall" href="javascript:;">' . Text::_('JNONE') . '</a>
</span>
<span class="nr_treeselect_control">' . Text::_('JSHOW') . ':
<a class="nr_treeselect-showall" href="javascript:;">' . Text::_('JALL') . '</a>,
<a class="nr_treeselect-showselected" href="javascript:;">' . Text::_('NR_SELECTED') . '</a>
</span>
<span class="nr_treeselect_control nr_treeselect-filter right">
<input type="text" name="nr_treeselect-filter" class="search-query nr_treeselect-filter" size="16"
autocomplete="off" placeholder="' . Text::_('JSEARCH_FILTER') . '" aria-invalid="false" tabindex="-1">
</span>
</div>';
$o = array();
foreach ($options as $option)
{
$option->level = isset($option->level) ? $option->level : 0;
$o[] = $option;
if (isset($option->links))
{
foreach ($option->links as $link)
{
$link->level = $option->level + (isset($link->level) ? $link->level : 1);
$o[] = $link;
}
}
}
$html[] = '<ul class="nr_treeselect-ul" style="max-height:300px;min-width:' . $size . 'px;overflow-x: hidden;">';
$prevlevel = 0;
foreach ($o as $i => $option)
{
if ($prevlevel < $option->level)
{
// correct wrong level indentations
$option->level = $prevlevel + 1;
$html[] = '<ul class="nr_treeselect-sub">';
}
else if ($prevlevel > $option->level)
{
$html[] = str_repeat('</li></ul>', $prevlevel - $option->level);
}
else if ($i)
{
$html[] = '</li>';
}
$labelclass = trim('pull-left ' . (isset($option->labelclass) ? $option->labelclass : ''));
$html[] = '<li>';
$item = '<div class="' . trim('nr_treeselect-item pull-left ' . (isset($option->class) ? $option->class : '')) . '">';
if (isset($option->title))
{
$labelclass .= ' nav-header';
}
if (isset($option->title) && (!isset($option->value) || !$option->value))
{
$item .= '<label class="' . $labelclass . '">' . $option->title . '</label>';
}
else
{
$selected = in_array($option->value, $value) ? ' checked="checked"' : '';
$disabled = (isset($option->disable) && $option->disable) ? ' readonly="readonly" style="visibility:hidden"' : '';
$item .= '<input type="checkbox" class="pull-left" name="' . $name . '" id="' . $id . $option->value . '" value="' . $option->value . '"' . $selected . $disabled . '>
<label for="' . $id . $option->value . '" class="' . $labelclass . '">' . $option->text . '</label>';
}
$item .= '</div>';
$html[] = $item;
if (!isset($o[$i + 1]) && $option->level > 0)
{
$html[] = str_repeat('</li></ul>', (int) $option->level);
}
$prevlevel = $option->level;
}
$html[] = '</ul>';
$html[] = '
<div style="display:none;" class="nr_treeselect-menu-block">
<div class="pull-left nav-hover nr_treeselect-menu">
<div class="btn-group">
<a href="#" data-toggle="dropdown" data-bs-toggle="dropdown" class="dropdown-toggle btn btn-secondary">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li class="nav-header">' . Text::_('COM_MODULES_SUBITEMS') . '</li>
<li class="divider"></li>
<li>
<a class="checkall" href="javascript:;">
<span class="icon-checkbox"></span>
' . Text::_('JSELECT') . '
</a>
</li>
<li>
<a class="uncheckall" href="javascript:;">
<span class="icon-checkbox-unchecked"></span>
' . Text::_('COM_MODULES_DESELECT') . '
</a>
</li>
<div class="nr_treeselect-menu-expand">
<li class="divider"></li>
<li><a class="expandall" href="javascript:;"><span class="icon-plus"></span> ' . Text::_('NR_EXPAND') . '</a></li>
<li><a class="collapseall" href="javascript:;"><span class="icon-minus"></span> ' . Text::_('NR_COLLAPSE') . '</a></li>
</div>
</ul>
</div>
</div>
</div>';
$html[] = '</div>';
$html = implode('', $html);
return $html;
}
public static function treeselectSimple(&$options, $name, $value, $id, $size = 300, $class = '')
{
return self::treeselect($options, $name, $value, $id, $size, 1, $class);
}
/**
* Wrapper for the HTMLHelper::script method to support old method signatures in Joomla < 3.7.0.
*
* @param string $path
*
* @deprecated Since we no longer support 3.7.0, use HTMLHelper::script directly.
* @return void
*/
public static function script($path)
{
if (version_compare(JVERSION, '3.7.0', 'lt'))
{
HTMLHelper::script($path, false, true);
} else
{
HTMLHelper::script($path, ['relative' => true, 'version' => 'auto']);
}
}
/**
* Wrapper for the HTMLHelper::stylesheet method to support old method signatures in Joomla < 3.7.0.
*
* @param string $path
*
* @return void
* @deprecated Since we no longer support 3.7.0, use HTMLHelper::script directly.
*/
public static function stylesheet($path)
{
if (version_compare(JVERSION, '3.7.0', 'lt'))
{
HTMLHelper::stylesheet($path, false, true);
} else
{
HTMLHelper::stylesheet($path, ['relative' => true, 'version' => 'auto']);
}
}
/**
* For Backwards Compatibility
*
* @deprecated 4.9.50
*/
public static function checkForOutdatedExtension($extension, $days_old = 120)
{
if (!Extension::isOutdated($extension, $days_old))
{
return;
}
// Load extension's language file
Functions::loadLanguage($extension);
$payload = [
'extension' => Text::_($extension),
'days_old' => $days_old
];
// load template
return LayoutHelper::render('outdated_extension', $payload, dirname(__DIR__) . '/layouts');
}
public static function updateNotification($extension)
{
$version_installed = Extension::getVersion($extension);
$version_latest = Extension::getLatestVersion($extension);
if (!$needsUpdate = version_compare($version_latest, $version_installed, 'gt'))
{
return;
}
// Load extension's language file
Functions::loadLanguage($extension);
// Extension Title
$title = Text::_($extension);
$title = str_replace('System -', '', $title); // Remove plugin folder prefix from plugins
// Render Layout
$data = [
'title' => $title,
'version_installed' => $version_installed,
'version_latest' => $version_latest,
'ispro' => Extension::isPro($extension),
'upgradeurl' => Extension::getTassosExtensionUpgradeURL($extension),
'product_url' => Extension::getProductURL($extension)
];
return LayoutHelper::render('updatechecker', $data, JPATH_PLUGINS . '/system/nrframework/layouts');
}
/**
* TODO: Not used anywhere, should delete.
*
* @deprecated 4.11.7
*/
public static function checkForUpdates($element)
{
HTMLHelper::script('plg_system_nrframework/updatechecker.js', ['relative' => true, 'version' => true]);
HTMLHelper::stylesheet('plg_system_nrframework/updatechecker.css', ['relative' => true, 'version' => true]);
return '
<div class="nr_updatechecker"
data-element="' . $element. '"
data-base=' . Uri::base() . '
data-token=' . Session::getFormToken() . '>
</div>
';
}
} Functions.php 0000644 00000050416 15235314576 0007246 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use \NRFramework\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Uri\Uri;
use Joomla\Filesystem\File;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Helper\ModuleHelper;
class Functions
{
/**
* Add HTML before the closing </body> tag.
*
* @param string $html The HTML to prepend to </body>
* @param boolean $once If true, the HTML will be added only once.
*
* @return void
*/
public static function appendToBody($html, $once = false)
{
if ($once)
{
$hash = md5($html);
if (Cache::has($hash))
{
return;
}
Cache::set($hash, true);
}
$app = Factory::getApplication();
$app->registerEvent('onAfterRender', function() use ($app, $html)
{
$buffer = $app->getBody();
$closingTag = '</body>';
if (strpos($buffer, $closingTag))
{
// If </body> exists prepend the given HTML
$buffer = str_replace($closingTag, $html . $closingTag, $buffer);
} else
{
// If </body> does not exist append to document's end
$buffer .= $html;
}
$app->setBody($buffer);
});
}
/**
* Fix arrays, remove duplicate items, null items and whitespace around item values.
*
* @param array $subject
*
* @return array The new cleaned array
*/
public static function cleanArray($subject)
{
if (!is_array($subject))
{
return $subject;
}
$subject = array_map(function($str)
{
return is_null($str) ? '' : trim($str);
}, $subject);
$subject = array_unique($subject);
// Remove empty items. We use a custom callback here because the default behavior of array_filter removes 0 values as well.
$subject = array_filter($subject, function($value)
{
return ($value !== null && $value !== false && $value !== '');
});
return $subject;
}
/**
* Attempt to convert a subject to array
*
* @param mixed $subject
*
* @return array
*/
public static function makeArray($subject)
{
if (empty($subject))
{
return [];
}
if (is_object($subject))
{
return (array) $subject;
}
if (!is_array($subject))
{
// replace newlines with commas
$subject = str_replace(PHP_EOL, ',', $subject);
// split keywords on commas
$subject = explode(',', $subject);
}
// Now that we have an array, run some housekeeping.
$arr = $subject;
$arr = self::cleanArray($arr);
// Reset keys
$arr = array_values($arr);
return $arr;
}
/**
* Return the real site base URL by ignoring the live_site configuration option.
*
* @param bool $ignore_admin If enabled and we are browsing administrator, we will get the front-end site root URL.
*
* @return string
*/
public static function getRootURL($ignore_admin = true)
{
$factory = Factory::getConfig();
// Store the original live_site value
$live_site_original = $factory->get('live_site', '');
// If we live_site is not set, do not proceed further. Return the default website base URL.
if (empty($live_site_original))
{
return $ignore_admin ? Uri::root() : Uri::base();
}
// Remove the live site
$factory->set('live_site', '');
// Remove all cached Uri instances
Uri::reset();
// Get a new URL. The live_site option should be ignored.
$base_url = $ignore_admin ? Uri::root() : Uri::base();
// Set back the original live_site
$factory->set('live_site', $live_site_original);
Uri::reset();
return $base_url;
}
/**
* Insert an associative array into a specific position in an array
*
* @param $original array The original array to add to
* @param $new array The new array of values to insert into the original
* @param $offset int The position in the array ( 0 index ) where the new array should go
*
* @return array The new combined array
*/
public static function array_splice_assoc($original,$new,$offset)
{
return array_slice($original, 0, $offset, true) + $new + array_slice($original, $offset, NULL, true);
}
public static function renderField($fieldname)
{
$fieldname = strtolower($fieldname);
require_once JPATH_PLUGINS . '/system/nrframework/fields/' . $fieldname . '.php';
$classname = '\JFormField' . $fieldname;
$field = new $classname();
$element = new \SimpleXMLElement('
<field name="' . $classname . '" type="' . $classname . '"
/>');
$field->setup($element, null);
return $field->__get('input');
}
/**
* Checks if an array of values (needle) exists in a text (haystack)
*
* @param array $needle The searched array of values.
* @param string $haystack The text
* @param bool $case_insensitive Indicates whether the letter case plays any role
*
* @return bool
*/
public static function strpos_arr($needles, $haystack, $case_insensitive = false)
{
$needles = !is_array($needles) ? (array) $needles : $needles;
$haystack = $case_insensitive ? strtolower($haystack) : $haystack;
foreach ($needles as $needle)
{
$needle = $case_insensitive ? strtolower($needle) : $needle;
if (strpos($haystack, $needle) !== false)
{
// stop on first true result
return true;
}
}
return false;
}
/**
* Log message to framework's log file
*
* @param mixed $data Log message
*
* @return void
*
* @deprecated Stop using method
*/
public static function log($data)
{
}
/**
* Return's a URL with the Google Analytics Campaign Parameters appended to the end
*
* @param string $url The URL
* @param string $medium Campaign Medium
* @param string $campaign Campaign Name
*
* @return string
*/
public static function getUTMURL($url, $medium = 'upgradebutton', $campaign = 'freeversion')
{
if (!$url)
{
return;
}
$utm = 'utm_source=CustomerBackend&utm_medium=' . $medium . '&utm_campaign=' . $campaign;
$char = strpos($url, '?') === false ? '?' : '&';
return $url . $char . $utm;
}
/**
* Returns user's Download Key
*
* @return string
*/
public static function getDownloadKey()
{
$class = new Updatesites();
return $class->getDownloadKey();
}
/**
* Adds a script or a stylesheet to the document
*
* @param Mixed $files The files to be to added to the document
* @param boolean $appendVersion Adds file versioning based on extension's version
*
* @return void
*/
public static function addMedia($files, $extension = "plg_system_nrframework", $appendVersion = true)
{
$doc = Factory::getDocument();
$version = self::getExtensionVersion($extension);
$mediaPath = Uri::root(true) . "/media/" . $extension;
if (!is_array($files))
{
$files = array($files);
}
foreach ($files as $key => $file)
{
$fileExt = File::getExt($file);
$filename = $mediaPath . "/" . $fileExt . "/" . $file;
$filename = ($appendVersion) ? $filename . "?v=" . $version : $filename;
if ($fileExt == "js")
{
$doc->addScript($filename);
}
if ($fileExt == "css")
{
$doc->addStylesheet($filename);
}
}
}
/**
* Get the Framework version
*
* @return string The framework version
*/
public static function getVersion()
{
return self::getExtensionVersion("plg_system_nrframework");
}
/**
* Checks if document is a feed document (xml, rss, atom)
*
* @return boolean
*/
public static function isFeed()
{
return (
Factory::getDocument()->getType() == 'feed'
|| Factory::getDocument()->getType() == 'xml'
|| Factory::getApplication()->input->getWord('format') == 'feed'
|| Factory::getApplication()->input->getWord('type') == 'rss'
|| Factory::getApplication()->input->getWord('type') == 'atom'
);
}
public static function loadLanguage($extension = 'plg_system_nrframework', $basePath = '')
{
if ($basePath && Factory::getLanguage()->load($extension, $basePath))
{
return true;
}
$basePath = self::getExtensionPath($extension, $basePath, 'language');
return Factory::getLanguage()->load($extension, $basePath);
}
/**
* Returns extension ID
*
* @param string $extension Extension name
*
* @return integer
*
* @deprecated Use \NRFramework\Extension::getID instead
*/
public static function getExtensionID($extension, $folder = null)
{
$type = is_null($folder) ? 'component' : 'plugin';
return \NRFramework\Extension::getID($extension, $type, $folder);
}
/**
* Checks if extension is installed
*
* @param string $extension The extension element name
* @param string $type The extension's type
* @param string $folder Plugin folder *
*
* @return boolean Returns true if extension is installed
*
* @deprecated Use \NRFramework\Extension::isInstalled instead
*/
public static function extensionInstalled($extension, $type = 'component', $folder = 'system')
{
return \NRFramework\Extension::isInstalled($extension, $type, $folder);
}
/**
* Returns the version number from the extension's xml file
*
* @param string $extension The extension element name
*
* @return string Extension's version number
*/
public static function getExtensionVersion($extension, $type = false)
{
$hash = MD5($extension . "_" . ($type ? "1" : "0"));
$cache = Cache::read($hash);
if ($cache)
{
return $cache;
}
$xml = self::getExtensionXMLFile($extension);
if (!$xml)
{
return false;
}
$xml = Installer::parseXMLInstallFile($xml);
if (!$xml || !isset($xml['version']))
{
return '';
}
$version = $xml['version'];
if ($type)
{
$extType = Extension::isPro($extension) ? 'Pro' : 'Free';
$version = $xml["version"] . " " . $extType;
}
return Cache::set($hash, $version);
}
public static function getExtensionXMLFile($extension, $basePath = JPATH_ADMINISTRATOR)
{
$alias = explode("_", $extension);
$alias = end($alias);
$filename = (strpos($extension, 'mod_') === 0) ? "mod_" . $alias : $alias;
$file = self::getExtensionPath($extension, $basePath) . "/" . $filename . ".xml";
if (file_exists($file))
{
return $file;
}
return false;
}
/**
* @deprecated // Use Extension::isPro();
*/
public static function extensionHasProInstalled($extension)
{
return Extension::isPro($extension);
}
public static function getExtensionPath($extension = 'plg_system_nrframework', $basePath = JPATH_ADMINISTRATOR, $check_folder = '')
{
$path = '';
switch (true)
{
case (strpos($extension, 'com_') === 0):
$path = 'components/' . $extension;
break;
case (strpos($extension, 'mod_') === 0):
$path = 'modules/' . $extension;
break;
case (strpos($extension, 'plg_system_') === 0):
$path = 'plugins/system/' . substr($extension, strlen('plg_system_'));
break;
case (strpos($extension, 'plg_editors-xtd_') === 0):
$path = 'plugins/editors-xtd/' . substr($extension, strlen('plg_editors-xtd_'));
break;
}
if (empty($path))
{
return;
}
$check_folder = $check_folder ? '/' . $check_folder : '';
$basePath = empty($basePath) ? JPATH_ADMINISTRATOR : $basePath;
if (is_dir($basePath . '/' . $path . $check_folder))
{
return $basePath . '/' . $path;
}
if (is_dir(JPATH_ADMINISTRATOR . '/' . $path . $check_folder))
{
return JPATH_ADMINISTRATOR . '/' . $path;
}
if (is_dir(JPATH_SITE . '/' . $path . $check_folder))
{
return JPATH_SITE . '/' . $path;
}
return $basePath;
}
public static function renderModulePosition($position, $style = 'custom')
{
$modules = ModuleHelper::getModules($position);
$attribs['style'] = $style;
foreach ($modules as $module)
{
echo ModuleHelper::renderModule($module, $attribs);
}
}
public static function loadModule($id, $moduleStyle = null)
{
// Return if no module id passed
if (!$id)
{
return;
}
// Fetch module from db
$db = Factory::getDBO();
$query = $db->getQuery(true)
->select('*')
->from('#__modules')
->where('id='.$db->q($id));
$db->setQuery($query);
// Return if no modules found
if (!$module = $db->loadObject())
{
return;
}
// Success! Return module's html
return ModuleHelper::renderModule($module, $moduleStyle);
}
public static function fixDate(&$date)
{
if (!$date)
{
$date = null;
return;
}
$date = trim($date);
// Check if date has correct syntax: 00-00-00 00:00:00
if (preg_match('#^[0-9]+-[0-9]+-[0-9]+( [0-9][0-9]:[0-9][0-9]:[0-9][0-9])$#', $date))
{
return;
}
// Check if date has syntax: 00-00-00 00:00
// If so, add :00 (seconds)
if (preg_match('#^[0-9]+-[0-9]+-[0-9]+ [0-9][0-9]:[0-9][0-9]$#', $date))
{
$date .= ':00';
return;
}
// Check if date has a prepending date syntax: 00-00-00 ...
// If so, add 00:00:00 (hours:mins;secs)
if (preg_match('#^([0-9]+-[0-9]+-[0-9]+)#', $date, $match))
{
$date = $match[1] . ' 00:00:00';
return;
}
// Date format is not correct, so return null
// $date = null;
}
/**
* Change date's timezone to UTC by modyfing the offset
*
* @param string $date The date in timezone other than UTC
*
* @return string The date in UTC
*/
public static function dateToUTC($date)
{
$date = is_string($date) ? trim($date) : $date;
if (empty($date) || is_null($date) || $date == '0000-00-00 00:00:00')
{
return $date;
}
$timezone = Factory::getUser()->getParam('timezone', Factory::getConfig()->get('offset'));
$date = new Date($date, $timezone);
$date->setTimezone(new \DateTimeZone('UTC'));
$dateUTC = $date->format('Y-m-d H:i:s', true, false);
return $dateUTC;
}
/**
* Applies the site's or the user's timezone to a given date.
*
* @param string $date
* @param string $format
*
* @return string
*/
public static function applySiteTimezoneToDate($date, $format = 'Y-m-d H:i:s')
{
$timezone = new \DateTimeZone(Factory::getUser()->getParam('timezone', Factory::getConfig()->get('offset')));
return Factory::getDate($date)->setTimezone($timezone)->format($format, true);
}
/**
* Change date's timezone to UTC by modyfing the offset
*
* @param string $date The date in timezone other than UTC
*
* @return string The date in UTC
*
* @deprecated Use dateToUTC()
*/
public static function fixDateOffset(&$date)
{
$date = self::dateToUTC($date);
}
// Text
public static function clean($string)
{
$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
}
public static function dateTimeNow()
{
return Factory::getDate()->format("Y-m-d H:i:s");
}
/**
* Get framework plugin's parameters
*
* @return Registry The plugin parameters
*/
public static function params()
{
$hash = md5('frameworkParams');
if (Cache::has($hash))
{
return Cache::read($hash);
}
$db = Factory::getDBO();
$result = $db->setQuery(
$db->getQuery(true)
->select('params')
->from('#__extensions')
->where('element = ' . $db->quote('nrframework'))
)->loadResult();
return Cache::set($hash, new Registry($result));
}
/**
* Checks whether string starts with substring.
*
* @param string $string
* @param string $query
*
* @return bool
*/
public static function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
}
/**
* Checks whether string end with substring.
*
* @param string $string
* @param string $substring
*
* @return bool
*/
public static function endsWith($string, $substring)
{
$length = strlen($substring);
return substr((string) $string, -$length) === $substring;
}
/**
* Updates the Download Key in the framework plugin.
*
* @param string $key
*
* @return bool
*/
public static function updateDownloadKey($key)
{
if (empty($key))
{
return false;
}
// Update params
$db = Factory::getDBO();
// Get params
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('element') . ' = ' . $db->quote('nrframework'));
$db->setQuery($query);
$params = $db->loadResult();
$params = json_decode($params, true);
// Set Download Key
$params['key'] = $key;
// Update params
$query->clear()
->update('#__extensions')
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('element') . ' = ' . $db->quote('nrframework'));
$db->setQuery($query);
$db->execute();
return true;
}
/**
* Return the current page's body regardless the system event fires the method.
*
* @return string
*/
public static function getBuffer()
{
$app = Factory::getApplication();
// Try to get the whole body first. This will only work if the method is executed in the onAfterRender event.
if ($body = $app->getBody())
{
return $body;
}
// We got an empty body. Probably, the method runs before the onAfterRender event. Let's give it another try.
$buffer = $app->getDocument()->getBuffer();
if (!is_array($buffer))
{
return;
}
$flatted = \Joomla\Utilities\ArrayHelper::flatten($buffer);
return implode(' ', $flatted);
}
} URLHelper.php 0000644 00000015561 15235314576 0007102 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use NRFramework\URL;
defined('_JEXEC') or die('Restricted access');
class URLHelper
{
/**
* Searches the given HTML for all external links and appends the affiliate paramter aff=id to every link based on an affiliate list.
*
* @param string $text The html to search for external links
* @param array $affiliates A key value array: domain name => affiliate parameter
*
* @return string
*/
public static function replaceAffiliateLinks($text, $affiliates, $factory = null)
{
if (!class_exists('DOMDocument') || empty($text))
{
return $text;
}
$factory = $factory ? $factory : new \NRFramework\Factory();
libxml_use_internal_errors(true);
$dom = new \DOMDocument;
$dom->encoding = 'UTF-8';
$dom->loadHTML($text);
$links = $dom->getElementsByTagName('a');
foreach ($links as $link)
{
$linkHref = $link->getAttribute('href');
if (empty($linkHref))
{
continue;
}
$url = new URL($linkHref, $factory);
if ($url->isInternal())
{
continue;
}
$domain = $url->getDomainName();
if (!array_key_exists($domain, $affiliates))
{
continue;
}
$urlInstance = $url->getInstance();
$urlQuery = $urlInstance->getQuery();
$affQuery = $affiliates[$domain];
// If both queries are the same, skip the link tag
if ($urlQuery === $affQuery)
{
continue;
}
if (empty($urlQuery))
{
$urlInstance->setQuery($affQuery);
} else
{
parse_str($urlQuery, $params);
parse_str($affQuery, $params_);
$params_new = array_merge($params, $params_);
$urlInstance->setQuery(http_build_query($params_new));
}
$newURL = $urlInstance->toString();
if ($newURL === $linkHref)
{
continue;
}
$link->setAttribute('href', $newURL);
}
return $dom->saveHtml();
}
/**
* Convert all <img> and <a> tags with relative paths to absolute URLs
*
* @param string $text The text/HTML to search for relative paths
* @param object $factory The framework's factory
* @param object $fix_links Should we parse links?
* @param object $fix_images Should we parse images?
*
* @return void The converted HTML string
*/
public static function relativePathsToAbsoluteURLs($text, $factory = null, $fix_links = true, $fix_images = true)
{
// Make sure DOMDocument is installed
if (!class_exists('DOMDocument'))
{
return $text;
}
// Quick check the given text has some links or images
$hasImages = $fix_images && strpos($text, '<img') !== false;
$hasLinks = $fix_links && strpos($text, '<a') !== false;
if (empty($text) || (!$hasImages && !$hasLinks))
{
return $text;
}
$factory = $factory ? $factory : new \NRFramework\Factory();
$replacements = 0;
try
{
libxml_use_internal_errors(true);
$dom = new \DOMDocument;
$dom->encoding = 'UTF-8';
// Handle non-latin characters to UTF8
$text_ = iconv('UTF-8', 'UTF-8', $text);
$text_ = mb_encode_numericentity($text_, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
// Load HTML without adding a doctype.
// Do not ever try to remove <html><body> tags with LIBXML_HTML_NOIMPLIED constant as it's rather unstable.
// https://stackoverflow.com/questions/4879946/how-to-savehtml-of-domdocument-without-html-wrapper/44866403#44866403
// LIBXML_HTML_NODEFDTD requires Libxml >= 2.7.8 - https://www.php.net/manual/en/libxml.constants.php
$dom->loadHTML($text_, LIBXML_HTML_NODEFDTD);
// Replace links
if ($fix_links)
{
$links = $dom->getElementsByTagName('a');
foreach ($links as $link)
{
$resource = $link->getAttribute('href');
if (empty($resource) || mb_substr($resource, 0, 1) == '#')
{
continue;
}
$url = new URL($resource, $factory);
if (!$url->isInternal())
{
continue;
}
$newURL = $url->toAbsolute();
$link->setAttribute('href', $newURL);
$replacements++;
}
}
// Replace images
if ($fix_images)
{
$images = $dom->getElementsByTagName('img');
foreach ($images as $image)
{
$resource = $image->getAttribute('src');
if (empty($resource))
{
continue;
}
$url = new URL($resource, $factory);
if (!$url->isInternal())
{
continue;
}
$newURL = $url->toAbsolute();
$image->setAttribute('src', $newURL);
$replacements++;
}
}
// If we don't have any replacements took place, proceed no further and return the original text.
if ($replacements == 0)
{
return $text;
}
$html = trim($dom->saveHTML($dom->documentElement));
// Make sure no <body> or <html> tags are added in the text
// In case the final string starts with <html><body>, we assume the elements are added by DOMDocument incorectly and we remove them.
// In case the final string starts with <html lang="en-gb" dir="ltr"><head>..., we assume the elements are included in the original text and we must leave them.
if (strpos($html, '<html><body>') !== false)
{
$html = str_replace(['<html><body>', '</body></html>'], '', $html);
}
return $html;
} catch (\Throwable $th)
{
return $text;
}
}
} Assignments.php 0000644 00000030534 15235314576 0007570 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use NRFramework\Factory;
use NRFramework\Conditions\ConditionsHelper;
defined('_JEXEC') or die;
// @deprecated - Use \NRFramework\Condnitions\ConditionsHelper;
class Assignments
{
/**
* Assignment Type Aliases
*
* @var array
*
* @deprecated To be removed on Jan 1st 2023.
*/
public $typeAliases = array(
'device|devices' => 'Device',
'urls|url' => 'URL',
'os' => 'OS',
'browsers|browser' => 'Browser',
'referrer' => 'Referrer',
'php' => 'PHP',
'timeonsite' => 'TimeOnSite',
'pageviews|user_pageviews' => 'Pageviews',
'lang|language|languages' => 'Joomla\Language',
'usergroups|usergroup|user_groups' => 'Joomla\UserGroup',
'user_id|userid' => 'Joomla\UserID',
'menu' => 'Joomla\Menu',
'components|component' => 'Joomla\Component',
'datetime|daterange|date' => 'Date\Date',
'weekday|days|day' => 'Date\Day',
'months|month' => 'Date\Month',
'timerange|time' => 'Date\Time',
'acymailing' => 'AcyMailing',
'akeebasubs' => 'AkeebaSubs',
'engagebox|onotherbox' => 'EngageBox',
'convertforms' => 'ConvertForms',
'geo_country|country|countries' => 'Geo\Country',
'geo_continent|continent|continents' => 'Geo\Continent',
'geo_city|city|cities' => 'Geo\City',
'geo_region|region|regions' => 'Geo\Region',
'cookiename|cookie' => 'Cookie',
'ip_addresses|iprange|ip' => 'IP',
'k2_items|k2item' => 'Component\K2Item',
'k2_cats|k2category' => 'Component\K2Category',
'k2_tags|k2tag' => 'Component\K2Tag',
'k2_pagetypes|k2pagetype' => 'Component\K2Pagetype',
'contentcats|category' => 'Component\ContentCategory',
'contentarticles|article' => 'Component\ContentArticle',
'contentview' => 'Component\ContentView',
'eventbookingsingle' => 'Component\EventBookingSingle',
'eventbookingcategory' => 'Component\EventBookingCategory',
'j2storesingle' => 'Component\J2StoreSingle',
'j2storecategory' => 'Component\J2StoreCategory',
'hikashopsingle' => 'Component\HikashopSingle',
'hikashopcategory' => 'Component\HikashopCategory',
'sppagebuildersingle' => 'Component\SPPageBuilderSingle',
'sppagebuildercategory' => 'Component\SPPageBuilderCategory',
'virtuemartcategory' => 'Component\VirtueMartCategory',
'virtuemartsingle' => 'Component\VirtueMartSingle',
'jshoppingsingle' => 'Component\JShoppingSingle',
'jshoppingcategory' => 'Component\JShoppingCategory',
'rsblogsingle' => 'Component\RSBlogSingle',
'rsblogcategory' => 'Component\RSBlogCategory',
'rseventsprosingle' => 'Component\RSEventsProSingle',
'rseventsprocategory' => 'Component\RSEventsProCategory',
'easyblogcategory' => 'Component\EasyBlogCategory',
'easyblogsingle' => 'Component\EasyBlogSingle',
'zoosingle' => 'Component\ZooSingle',
'zoocategory' => 'Component\ZooCategory',
'eshopcategory' => 'Component\EshopCategory',
'eshopsingle' => 'Component\EshopSingle',
'jeventssingle' => 'Component\JEventsSingle',
'jeventscategory' => 'Component\JEventsCategory',
'djcatalog2category' => 'Component\DJCatalog2Category',
'djcatalog2single' => 'Component\DJCatalog2Single',
'quixsingle' => 'Component\QuixSingle',
'djclassifiedssingle' => 'Component\DJClassifiedsSingle',
'djclassifiedscategory' => 'Component\DJClassifiedsCategory',
'sobiprocategory' => 'Component\SobiProCategory',
'sobiprosingle' => 'Component\SobiProSingle',
'gridboxcategory' => 'Component\GridboxCategory',
'gridboxsingle' => 'Component\GridboxSingle',
'djeventscategory' => 'Component\DJEventsCategory',
'djeventssingle' => 'Component\DJEventsSingle',
'jcalprocategory' => 'Component\JCalProCategory',
'jcalprosingle' => 'Component\JCalProSingle',
'dpcalendarcategory' => 'Component\DPCalendarCategory',
'dpcalendarsingle' => 'Component\DPCalendarSingle',
'icagendacategory' => 'Component\ICagendaCategory',
'icagendasingle' => 'Component\ICagendaSingle',
'jbusinessdirectorybusinesscategory' => 'Component\JBusinessDirectoryBusinessCategory',
'jbusinessdirectorybusinesssingle' => 'Component\JBusinessDirectoryBusinessSingle',
'jbusinessdirectoryeventcategory' => 'Component\JBusinessDirectoryEventCategory',
'jbusinessdirectoryeventsingle' => 'Component\JBusinessDirectoryEventSingle',
'jbusinessdirectoryoffercategory' => 'Component\JBusinessDirectoryOfferCategory',
'jbusinessdirectoryoffersingle' => 'Component\JBusinessDirectoryOfferSingle',
'jreviewscategory' => 'Component\JReviewsCategory',
'jreviewssingle' => 'Component\JReviewsSingle'
);
/**
* Factory object
*
* @var \NRFramework\Factory
*/
protected $factory;
/**
* Class constructor
*/
public function __construct($factory = null)
{
$this->factory = is_null($factory) ? new Factory() : $factory;
}
/**
* Legacy method to check a set of rules.
*
* At the moment of writing this and the moment we're going to release a new version for EngageBox
* which is going to introduce the new ConditionBuilder field, ACF and GSD will still be using the passAll() method.
*
* This forces us to keep this method for backwards compatibiliy reasons.
* Additionally, it helps us to catch a special case where both ACF and GSD expect to pass all rules even if the array passed is null.
*
* @param array|object $assignments_info Array/Object containing assignment info
* @param string $match_method The matching method (and|or) - Deprecated
* @param bool $debug Set to true to request additional debug information about assignments
*
* @deprecated Use passSets() instead. To be removed on Jan 1st 2023
*/
public function passAll($assignments_info, $match_method = 'and')
{
$assignments = $this->prepareAssignments($assignments_info, $match_method);
$ch = new ConditionsHelper($this->factory);
$pass = $ch->passSets($assignments);
// If the checks return null, consider this as Success. This is required for both ACF and GSD.
return is_null($pass) ? true : $pass;
}
/**
* Returns the classname for a given assignment alias
*
* @param string $alias
* @return string|void
*
* @deprecated To be removed on Jan 1st 2023
*/
public function aliasToClassname($alias)
{
$alias = strtolower($alias);
foreach ($this->typeAliases as $aliases => $type)
{
if (strtolower($type) == $alias)
{
return $type;
}
$aliases = explode('|', strtolower($aliases));
if (in_array($alias, $aliases))
{
return $type;
}
}
return null;
}
/**
* Checks and prepares the given array of assignment information
*
* @param array $assignments_info
* @return array
*
* @deprecated To be removed on Jan 1st 2023
*/
protected function prepareAssignments($data, $matching_method = 'all')
{
if (is_object($data))
{
return $this->prepareAssignmentsFromObject($data, $matching_method);
}
if (!is_array($data) OR empty($data))
{
return;
}
$rules = array_pop($data);
if (!is_array($rules) OR empty($rules))
{
return;
}
foreach ($rules as &$rule)
{
if (is_array($rule))
{
foreach ($rule as &$_rule)
{
$_rule = $this->prepareAssignmentRule($_rule);
}
}
else
{
$rule = $this->prepareAssignmentRule($rule);
}
}
$data = [
[
'matching_method' => $matching_method == 'and' ? 'all' : 'any',
'rules' => $rules
]
];
return $data;
}
/**
* Prepares the assignment rule.
*
* @param object $rule
*
* @return object
*/
private function prepareAssignmentRule($rule)
{
return [
'name' => $this->aliasToClassname($rule->alias),
'operator' => (int) $rule->assignment_state == 1 ? 'includes' : 'not_includes',
'value' => isset($rule->value) ? $rule->value : null,
'params' => isset($rule->params) ? $rule->params : null,
];
}
/**
* Converts an object of assignment information to an array of groups
* Used by existing extensions
*
* @param object $assignments_info
* @param string $matching_method
*
* @deprecated To be removed on Jan 1st 2023
*/
public function prepareAssignmentsFromObject($assignments_info, $matching_method)
{
if (!isset($assignments_info->params))
{
return [];
}
$params = json_decode($assignments_info->params);
if (!is_object($params))
{
return [];
}
$assignments_info = [];
foreach ($this->typeAliases as $aliases => $type)
{
$aliases = explode('|', $aliases);
foreach ($aliases as $alias)
{
if (!isset($params->{'assign_' . $alias}) || !$params->{'assign_' . $alias})
{
continue;
}
// Discover assignment params
$assignment_params = new \stdClass();
foreach ($params as $key => $value)
{
if (strpos($key, "assign_" . $alias . "_param") !== false)
{
$key = str_replace("assign_" . $alias . "_param_", "", $key);
$assignment_params->$key = $value;
}
}
$assignments_info[] = [
'name' => $this->aliasToClassname($alias),
'operator' => (int) $params->{'assign_' . $alias} == 1 ? 'includes' : 'not_includes',
'value' => isset($params->{'assign_' . $alias . '_list'}) ? $params->{'assign_' . $alias . '_list'} : [],
'params' => $assignment_params
];
}
}
$data = [
[
'matching_method' => $matching_method == 'and' ? 'all' : 'any',
'rules' => $assignments_info
]
];
return $data;
}
} SmartTags.php 0000644 00000000576 15235314576 0007205 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
/**
* This file is deprecated. Use \NRFramework\SmartTags\SmartTags instead.
*/
// No direct access
defined('_JEXEC') or die; User.php 0000644 00000004562 15235314576 0006215 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use NRFramework\Cache;
use Joomla\CMS\Factory;
class User
{
/**
* Return the user object
*
* @param mixed $id The primary key of the user
*
* @return mixed object on success, null on failure
*/
public static function get($id = null)
{
// Return current active user
if (is_null($id))
{
return Factory::getUser();
}
// Prevent Joomla from displaying a warning from missing user by checking if the user exists first
if (!self::exists($id))
{
return;
}
return Factory::getUser($id);
}
/**
* Checks whether the user does exist in the database
*
* @param integer $id The primary key of the user
*
* @return bool
*/
public static function exists($id)
{
$hash = 'user' . $id;
if (Cache::has($hash))
{
return Cache::get($hash);
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('count(id)')
->from('#__users')
->where('id = ' . $db->quote($id));
$db->setQuery($query);
// Cache result
return Cache::set($hash, $db->loadResult());
}
/**
* Get the IP address of the user
*
* @return string
*/
public static function getIP()
{
$server = Factory::getApplication()->input->server;
$ip = '';
// Whether ip is from the share internet
if (!empty($server->get('HTTP_CLIENT_IP')))
{
$ip = $server->get('HTTP_CLIENT_IP', '', 'string');
}
//whether ip is from the proxy
else if (!empty($server->get('HTTP_X_FORWARDED_FOR')))
{
$ip = $server->get('HTTP_X_FORWARDED_FOR', '', 'string');
}
else
{
$ip = $server->get('REMOTE_ADDR', '', 'string');
}
// Get the first IP if multiple were returned
$ips = explode(',', $ip);
$ip = reset($ips);
return $ip;
}
} Fonts.php 0000644 00000004321 15235314576 0006361 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined( '_JEXEC' ) or die( 'Restricted access' );
use Joomla\CMS\Factory;
/**
* Fonts Class
*/
class Fonts
{
/**
* Classic Fonts
*
* @var array
*/
private static $classic = array(
'Arial',
'Arial Black',
'Georgia',
'Tahoma',
'Franklin Gothic Medium',
'Calibri',
'Cambria',
'Century Gothic',
'Consolas',
'Corbel',
'Courier New',
'Times New Roman',
'Impact',
'Lucida Console',
'Palatino Linotype',
'Trebuchet MS',
'Verdana'
);
/**
* Google Fonts List
*
* @var array
*/
private static $google = array(
'Roboto',
'Staatliches',
'Thasadith',
'Open Sans',
'Sarabun',
'Slabo 27px',
'Lato',
'Oswald',
'Charm',
'Roboto Condensed',
'Source Sans Pro',
'Montserrat',
'Raleway',
'PT Sans',
'Poppins',
'Roboto Slab',
'Lora',
'Droid Sans',
'Merriweather',
'Ubuntu',
'Droid Serif',
'Arimo',
'Noto Sans',
'PT Sans Narro'
);
/**
* Returns all font groups alphabetically sorted
*
* @return array
*/
public static function getFontGroups()
{
return array(
'Google Fonts' => self::getFontGroup('google'),
'Classic' => self::getFontGroup('classic')
);
}
/**
* Returns a font group alphabetically sorted
*
* @param string $name The Font Group
*
* @return array
*/
public static function getFontGroup($name)
{
$fonts = self::$$name;
sort($fonts);
return $fonts;
}
/**
* Loads Google font to the document
*
* @param mixed $name The Google font name
*
* @return void
*/
public static function loadFont($names)
{
if (!$names)
{
return;
}
if (!is_array($names))
{
$names = array($names);
}
foreach ($names as $key => $value)
{
// If font is a Google Font then load it into the document
if (in_array($value, self::$google))
{
Factory::getDocument()->addStylesheet('//fonts.googleapis.com/css?family=' . urlencode($value));
}
}
}
} Countries.php 0000644 00000140076 15235314576 0007253 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use NRFramework\Cache;
defined('_JEXEC') or die('Restricted access');
/**
* Helper class to work with country names/codes
*/
class Countries
{
/**
* Countries List
*
* @deprecated: Use getCountriesData();
*
* @const array
*/
public static $map = [
'AF' => "Afghanistan",
'AX' => "Aland Islands",
'AL' => "Albania",
'DZ' => "Algeria",
'AS' => "American Samoa",
'AD' => "Andorra",
'AO' => "Angola",
'AI' => "Anguilla",
'AQ' => "Antarctica",
'AG' => "Antigua and Barbuda",
'AR' => "Argentina",
'AM' => "Armenia",
'AW' => "Aruba",
'AU' => "Australia",
'AT' => "Austria",
'AZ' => "Azerbaijan",
'BS' => "Bahamas",
'BH' => "Bahrain",
'BD' => "Bangladesh",
'BB' => "Barbados",
'BY' => "Belarus",
'BE' => "Belgium",
'BZ' => "Belize",
'BJ' => "Benin",
'BM' => "Bermuda",
'BQ-BO' => "Bonaire",
'BQ-SA' => "Saba",
'BQ-SE' => "Sint Eustatius",
'BT' => "Bhutan",
'BO' => "Bolivia",
'BA' => "Bosnia and Herzegovina",
'BW' => "Botswana",
'BV' => "Bouvet Island",
'BR' => "Brazil",
'IO' => "British Indian Ocean Territory",
'BN' => "Brunei Darussalam",
'BG' => "Bulgaria",
'BF' => "Burkina Faso",
'BI' => "Burundi",
'KH' => "Cambodia",
'CM' => "Cameroon",
'CA' => "Canada",
'CV' => "Cape Verde",
'KY' => "Cayman Islands",
'CF' => "Central African Republic",
'TD' => "Chad",
'CL' => "Chile",
'CN' => "China",
'CX' => "Christmas Island",
'CC' => "Cocos (Keeling) Islands",
'CO' => "Colombia",
'KM' => "Comoros",
'CG' => "Congo",
'CD' => "Congo, The Democratic Republic of the",
'CK' => "Cook Islands",
'CR' => "Costa Rica",
'CI' => "Cote d'Ivoire",
'HR' => "Croatia",
'CU' => "Cuba",
'CW' => "Curaçao",
'CY' => "Cyprus",
'CZ' => "Czech Republic",
'DK' => "Denmark",
'DJ' => "Djibouti",
'DM' => "Dominica",
'DO' => "Dominican Republic",
'EC' => "Ecuador",
'EG' => "Egypt",
'SV' => "El Salvador",
'GQ' => "Equatorial Guinea",
'ER' => "Eritrea",
'EE' => "Estonia",
'ET' => "Ethiopia",
'FK' => "Falkland Islands (Malvinas)",
'FO' => "Faroe Islands",
'FJ' => "Fiji",
'FI' => "Finland",
'FR' => "France",
'GF' => "French Guiana",
'PF' => "French Polynesia",
'TF' => "French Southern Territories",
'GA' => "Gabon",
'GM' => "Gambia",
'GE' => "Georgia",
'DE' => "Germany",
'GH' => "Ghana",
'GI' => "Gibraltar",
'GR' => "Greece",
'GL' => "Greenland",
'GD' => "Grenada",
'GP' => "Guadeloupe",
'GU' => "Guam",
'GT' => "Guatemala",
'GG' => "Guernsey",
'GN' => "Guinea",
'GW' => "Guinea-Bissau",
'GY' => "Guyana",
'HT' => "Haiti",
'HM' => "Heard Island and McDonald Islands",
'VA' => "Holy See (Vatican City State)",
'HN' => "Honduras",
'HK' => "Hong Kong",
'HU' => "Hungary",
'IS' => "Iceland",
'IN' => "India",
'ID' => "Indonesia",
'IR' => "Iran, Islamic Republic of",
'IQ' => "Iraq",
'IE' => "Ireland",
'IM' => "Isle of Man",
'IL' => "Israel",
'IT' => "Italy",
'JM' => "Jamaica",
'JP' => "Japan",
'JE' => "Jersey",
'JO' => "Jordan",
'KZ' => "Kazakhstan",
'KE' => "Kenya",
'KI' => "Kiribati",
'KP' => "Korea, Democratic People's Republic of",
'KR' => "Korea, Republic of",
'KW' => "Kuwait",
'KG' => "Kyrgyzstan",
'LA' => "Lao People's Democratic Republic",
'LV' => "Latvia",
'LB' => "Lebanon",
'LS' => "Lesotho",
'LR' => "Liberia",
'LY' => "Libyan Arab Jamahiriya",
'LI' => "Liechtenstein",
'LT' => "Lithuania",
'LU' => "Luxembourg",
'MO' => "Macao",
'MK' => "Macedonia",
'MG' => "Madagascar",
'MW' => "Malawi",
'MY' => "Malaysia",
'MV' => "Maldives",
'ML' => "Mali",
'MT' => "Malta",
'MH' => "Marshall Islands",
'MQ' => "Martinique",
'MR' => "Mauritania",
'MU' => "Mauritius",
'YT' => "Mayotte",
'MX' => "Mexico",
'FM' => "Micronesia, Federated States of",
'MD' => "Moldova, Republic of",
'MC' => "Monaco",
'MN' => "Mongolia",
'ME' => "Montenegro",
'MS' => "Montserrat",
'MA' => "Morocco",
'MZ' => "Mozambique",
'MM' => "Myanmar",
'NA' => "Namibia",
'NR' => "Nauru",
'NP' => "Nepal",
'NL' => "Netherlands",
'AN' => "Netherlands Antilles",
'NC' => "New Caledonia",
'NZ' => "New Zealand",
'NI' => "Nicaragua",
'NE' => "Niger",
'NG' => "Nigeria",
'NU' => "Niue",
'NF' => "Norfolk Island",
'NM' => "North Macedonia",
'MP' => "Northern Mariana Islands",
'NO' => "Norway",
'OM' => "Oman",
'PK' => "Pakistan",
'PW' => "Palau",
'PS' => "Palestinian Territory",
'PA' => "Panama",
'PG' => "Papua New Guinea",
'PY' => "Paraguay",
'PE' => "Peru",
'PH' => "Philippines",
'PN' => "Pitcairn",
'PL' => "Poland",
'PT' => "Portugal",
'PR' => "Puerto Rico",
'QA' => "Qatar",
'RE' => "Reunion",
'RO' => "Romania",
'RU' => "Russian Federation",
'RW' => "Rwanda",
'SH' => "Saint Helena",
'KN' => "Saint Kitts and Nevis",
'LC' => "Saint Lucia",
'PM' => "Saint Pierre and Miquelon",
'VC' => "Saint Vincent and the Grenadines",
'WS' => "Samoa",
'SM' => "San Marino",
'ST' => "Sao Tome and Principe",
'SA' => "Saudi Arabia",
'SN' => "Senegal",
'RS' => "Serbia",
'SC' => "Seychelles",
'SL' => "Sierra Leone",
'SG' => "Singapore",
'SK' => "Slovakia",
'SI' => "Slovenia",
'SB' => "Solomon Islands",
'SO' => "Somalia",
'ZA' => "South Africa",
'GS' => "South Georgia and the South Sandwich Islands",
'ES' => "Spain",
'LK' => "Sri Lanka",
'SD' => "Sudan",
'SS' => "South Sudan",
'SR' => "Suriname",
'SJ' => "Svalbard and Jan Mayen",
'SZ' => "Swaziland",
'SE' => "Sweden",
'CH' => "Switzerland",
'SY' => "Syrian Arab Republic",
'TW' => "Taiwan",
'TJ' => "Tajikistan",
'TZ' => "Tanzania, United Republic of",
'TH' => "Thailand",
'TL' => "Timor-Leste",
'TG' => "Togo",
'TK' => "Tokelau",
'TO' => "Tonga",
'TT' => "Trinidad and Tobago",
'TN' => "Tunisia",
'TR' => "Turkey",
'TM' => "Turkmenistan",
'TC' => "Turks and Caicos Islands",
'TV' => "Tuvalu",
'UG' => "Uganda",
'UA' => "Ukraine",
'AE' => "United Arab Emirates",
'GB' => "United Kingdom",
'US' => "United States",
'UM' => "United States Minor Outlying Islands",
'UY' => "Uruguay",
'UZ' => "Uzbekistan",
'VU' => "Vanuatu",
'VE' => "Venezuela",
'VN' => "Vietnam",
'VG' => "Virgin Islands, British",
'VI' => "Virgin Islands, U.S.",
'WF' => "Wallis and Futuna",
'EH' => "Western Sahara",
'YE' => "Yemen",
'ZM' => "Zambia",
'ZW' => "Zimbabwe",
];
/**
* Get information for given country
*
* @param string $countryCode
*
* @return array An assosiative array with country information
*/
public static function getCountry($countryCode)
{
$countries = self::getCountriesData();
$countryCode = \strtoupper($countryCode);
if (!isset($countries[$countryCode]))
{
return;
}
return array_merge($countries[$countryCode], [
'code' => $countryCode
]);
}
/**
* Attemp to convert a Country Code to a Country Name
*
* @param string $country_code The country code
*
* @return mixed String on success, Null on failure
*/
public static function toCountryName($country_code)
{
$countries = self::getCountriesList();
if (isset($countries[$country_code]))
{
return $countries[$country_code];
}
}
/**
* Attemp to convert a Country name to a Country code
*
* @param string $subject The country name
*
* @return mixed String on success, Null on failure
*/
public static function toCountryCode($subject)
{
$subject = strtolower($subject);
$cacheHash = md5('toCountryCode' . $subject);
if (Cache::has($cacheHash))
{
return Cache::get($cacheHash);
}
$countries = array_change_key_case(self::getCountriesList());
// Sanity check. Check first if we have a country code already.
if (array_key_exists($subject, $countries))
{
return strtoupper($subject);
}
// Let's find the country code in the list.
foreach ($countries as $country_code => $country_name)
{
if (strtolower($country_name) == $subject)
{
return strtoupper($country_code);
}
}
// Country code still not found. Probably we have a non-english country name.
// Let's load one by one all the language files and try to find it there.
$langFiles = \Joomla\Filesystem\Folder::files(JPATH_PLUGINS . '/system/nrframework/language', '.ini', 1, true);
foreach ($langFiles as $langFile)
{
$strings = LanguageHelper::parseIniFile($langFile);
foreach ($strings as $key => $label)
{
if (strpos($key, 'NR_COUNTRY_') === false)
{
continue;
}
if (strtolower($label) !== $subject)
{
continue;
}
// Found!
return Cache::set($cacheHash, str_replace('NR_COUNTRY_', '', $key));
}
}
}
/**
* Convert a Country Name to Country Code
*
* @param string $country The country name
*
* @return string|void
*/
public static function getCode($country)
{
$country = strtolower($country);
foreach (self::getCountriesList() as $key => $value)
{
if (strtolower($value) == $country)
{
return $key;
}
}
}
/**
* Returns translatable countries list
*
* @return array
*/
public static function getCountriesList()
{
$countries = [];
foreach (self::getCountriesData() as $key => $country)
{
$countries[$key] = $country['name'];
}
return $countries;
}
/**
* Returns a country's calling code.
*
* @param string $country_code
*
* @return string
*/
public static function getCallingCodeByCountryCode($country_code = '')
{
if (!$country_code)
{
return;
}
$countries = self::getCountriesData();
if (!isset($countries[$country_code]))
{
return;
}
return $countries[$country_code]['calling_code'];
}
/**
* Holds the following data for each country:
* - Name
* - Code
* - Calling Code
* - Currency Code
* - Curency Name
* - Currency Symbol
*
* @return array
*/
public static function getCountriesData()
{
$list = [
'AF' => [ 'name' => Text::_('NR_COUNTRY_AF'), 'calling_code' => '93', 'currency_code' => 'AFN', 'currency_name' => 'Afghan Afghani', 'currency_symbol' => '؋' ],
'AX' => [ 'name' => Text::_('NR_COUNTRY_AX'), 'calling_code' => '358', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'AL' => [ 'name' => Text::_('NR_COUNTRY_AL'), 'calling_code' => '355', 'currency_code' => 'ALL', 'currency_name' => 'Lek', 'currency_symbol' => 'Lek' ],
'DZ' => [ 'name' => Text::_('NR_COUNTRY_DZ'), 'calling_code' => '213', 'currency_code' => 'DZD', 'currency_name' => 'Dinar', 'currency_symbol' => 'دج' ],
'AS' => [ 'name' => Text::_('NR_COUNTRY_AS'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'AD' => [ 'name' => Text::_('NR_COUNTRY_AD'), 'calling_code' => '376', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'AO' => [ 'name' => Text::_('NR_COUNTRY_AO'), 'calling_code' => '244', 'currency_code' => 'AOA', 'currency_name' => 'Kwanza', 'currency_symbol' => 'Kz' ],
'AI' => [ 'name' => Text::_('NR_COUNTRY_AI'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'AQ' => [ 'name' => Text::_('NR_COUNTRY_AQ'), 'calling_code' => '672', 'currency_code' => '', 'currency_name' => '', 'currency_symbol' => '' ],
'AG' => [ 'name' => Text::_('NR_COUNTRY_AG'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'AR' => [ 'name' => Text::_('NR_COUNTRY_AR'), 'calling_code' => '54', 'currency_code' => 'ARS', 'currency_name' => 'Peso', 'currency_symbol' => '$' ],
'AM' => [ 'name' => Text::_('NR_COUNTRY_AM'), 'calling_code' => '374', 'currency_code' => 'AMD', 'currency_name' => 'Dram', 'currency_symbol' => '֏' ],
'AW' => [ 'name' => Text::_('NR_COUNTRY_AW'), 'calling_code' => '297', 'currency_code' => 'AWG', 'currency_name' => 'Guilder', 'currency_symbol' => 'ƒ' ],
'AU' => [ 'name' => Text::_('NR_COUNTRY_AU'), 'calling_code' => '61', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'AT' => [ 'name' => Text::_('NR_COUNTRY_AT'), 'calling_code' => '43', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'AZ' => [ 'name' => Text::_('NR_COUNTRY_AZ'), 'calling_code' => '994', 'currency_code' => 'AZN', 'currency_name' => 'Manat', 'currency_symbol' => 'ман' ],
'BS' => [ 'name' => Text::_('NR_COUNTRY_BS'), 'calling_code' => '1', 'currency_code' => 'BSD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BH' => [ 'name' => Text::_('NR_COUNTRY_BH'), 'calling_code' => '973', 'currency_code' => 'BHD', 'currency_name' => 'Dinar', 'currency_symbol' => 'د.ب' ],
'BD' => [ 'name' => Text::_('NR_COUNTRY_BD'), 'calling_code' => '880', 'currency_code' => 'BDT', 'currency_name' => 'Taka', 'currency_symbol' => '৳' ],
'BB' => [ 'name' => Text::_('NR_COUNTRY_BB'), 'calling_code' => '1', 'currency_code' => 'BBD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BY' => [ 'name' => Text::_('NR_COUNTRY_BY'), 'calling_code' => '375', 'currency_code' => 'BYR', 'currency_name' => 'Ruble', 'currency_symbol' => 'p.' ],
'BE' => [ 'name' => Text::_('NR_COUNTRY_BE'), 'calling_code' => '32', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'BZ' => [ 'name' => Text::_('NR_COUNTRY_BZ'), 'calling_code' => '501', 'currency_code' => 'BZD', 'currency_name' => 'Dollar', 'currency_symbol' => 'BZ$' ],
'BJ' => [ 'name' => Text::_('NR_COUNTRY_BJ'), 'calling_code' => '229', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'BM' => [ 'name' => Text::_('NR_COUNTRY_BM'), 'calling_code' => '1', 'currency_code' => 'BMD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BQ-BO' => [ 'name' => Text::_('NR_COUNTRY_BQ_BO'), 'calling_code' => '599', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BQ-SA' => [ 'name' => Text::_('NR_COUNTRY_BQ_SA'), 'calling_code' => '599', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BQ-SE' => [ 'name' => Text::_('NR_COUNTRY_BQ_SE'), 'calling_code' => '599', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BT' => [ 'name' => Text::_('NR_COUNTRY_BT'), 'calling_code' => '975', 'currency_code' => 'BTN', 'currency_name' => 'Ngultrum', 'currency_symbol' => 'Nu.' ],
'BO' => [ 'name' => Text::_('NR_COUNTRY_BO'), 'calling_code' => '591', 'currency_code' => 'BOB', 'currency_name' => 'Boliviano', 'currency_symbol' => '$b' ],
'BA' => [ 'name' => Text::_('NR_COUNTRY_BA'), 'calling_code' => '387', 'currency_code' => 'BAM', 'currency_name' => 'Marka', 'currency_symbol' => 'KM' ],
'BW' => [ 'name' => Text::_('NR_COUNTRY_BW'), 'calling_code' => '267', 'currency_code' => 'BWP', 'currency_name' => 'Pula', 'currency_symbol' => 'P' ],
'BV' => [ 'name' => Text::_('NR_COUNTRY_BV'), 'calling_code' => '47', 'currency_code' => 'NOK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'BR' => [ 'name' => Text::_('NR_COUNTRY_BR'), 'calling_code' => '55', 'currency_code' => 'BRL', 'currency_name' => 'Real', 'currency_symbol' => 'R$' ],
'IO' => [ 'name' => Text::_('NR_COUNTRY_IO'), 'calling_code' => '246', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'VG' => [ 'name' => Text::_('NR_COUNTRY_VG'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BN' => [ 'name' => Text::_('NR_COUNTRY_BN'), 'calling_code' => '673', 'currency_code' => 'BND', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'BG' => [ 'name' => Text::_('NR_COUNTRY_BG'), 'calling_code' => '359', 'currency_code' => 'BGN', 'currency_name' => 'Lev', 'currency_symbol' => 'лв' ],
'BF' => [ 'name' => Text::_('NR_COUNTRY_BF'), 'calling_code' => '226', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'BI' => [ 'name' => Text::_('NR_COUNTRY_BI'), 'calling_code' => '257', 'currency_code' => 'BIF', 'currency_name' => 'Franc', 'currency_symbol' => 'FBu' ],
'KH' => [ 'name' => Text::_('NR_COUNTRY_KH'), 'calling_code' => '855', 'currency_code' => 'KHR', 'currency_name' => 'Riels', 'currency_symbol' => '៛' ],
'CM' => [ 'name' => Text::_('NR_COUNTRY_CM'), 'calling_code' => '237', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCF' ],
'CA' => [ 'name' => Text::_('NR_COUNTRY_CA'), 'calling_code' => '1', 'currency_code' => 'CAD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'CV' => [ 'name' => Text::_('NR_COUNTRY_CV'), 'calling_code' => '238', 'currency_code' => 'CVE', 'currency_name' => 'Escudo', 'currency_symbol' => '$' ],
'KY' => [ 'name' => Text::_('NR_COUNTRY_KY'), 'calling_code' => '1', 'currency_code' => 'KYD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'CF' => [ 'name' => Text::_('NR_COUNTRY_CF'), 'calling_code' => '236', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCF' ],
'TD' => [ 'name' => Text::_('NR_COUNTRY_TD'), 'calling_code' => '235', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCFA' ],
'CL' => [ 'name' => Text::_('NR_COUNTRY_CL'), 'calling_code' => '56', 'currency_code' => 'CLP', 'currency_name' => 'Peso', 'currency_symbol' => '$' ],
'CN' => [ 'name' => Text::_('NR_COUNTRY_CN'), 'calling_code' => '86', 'currency_code' => 'CNY', 'currency_name' => 'YuanRenminbi', 'currency_symbol' => '¥' ],
'CX' => [ 'name' => Text::_('NR_COUNTRY_CX'), 'calling_code' => '61', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'CC' => [ 'name' => Text::_('NR_COUNTRY_CC'), 'calling_code' => '61', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'CO' => [ 'name' => Text::_('NR_COUNTRY_CO'), 'calling_code' => '57', 'currency_code' => 'COP', 'currency_name' => 'Peso', 'currency_symbol' => '$' ],
'KM' => [ 'name' => Text::_('NR_COUNTRY_KM'), 'calling_code' => '269', 'currency_code' => 'KMF', 'currency_name' => 'Franc', 'currency_symbol' => 'CF' ],
'CK' => [ 'name' => Text::_('NR_COUNTRY_CK'), 'calling_code' => '682', 'currency_code' => 'NZD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'CR' => [ 'name' => Text::_('NR_COUNTRY_CR'), 'calling_code' => '506', 'currency_code' => 'CRC', 'currency_name' => 'Colon', 'currency_symbol' => '₡' ],
'HR' => [ 'name' => Text::_('NR_COUNTRY_HR'), 'calling_code' => '385', 'currency_code' => 'HRK', 'currency_name' => 'Kuna', 'currency_symbol' => 'kn' ],
'CU' => [ 'name' => Text::_('NR_COUNTRY_CU'), 'calling_code' => '53', 'currency_code' => 'CUP', 'currency_name' => 'Peso', 'currency_symbol' => '₱' ],
'CW' => [ 'name' => Text::_('NR_COUNTRY_CW'), 'calling_code' => '599', 'currency_code' => 'ANG', 'currency_name' => 'Guilder', 'currency_symbol' => 'ƒ' ],
'CY' => [ 'name' => Text::_('NR_COUNTRY_CY'), 'calling_code' => '357', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'CZ' => [ 'name' => Text::_('NR_COUNTRY_CZ'), 'calling_code' => '420', 'currency_code' => 'CZK', 'currency_name' => 'Koruna', 'currency_symbol' => 'Kč' ],
'CD' => [ 'name' => Text::_('NR_COUNTRY_CD'), 'calling_code' => '243', 'currency_code' => 'CDF', 'currency_name' => 'Franc', 'currency_symbol' => 'FC' ],
'DK' => [ 'name' => Text::_('NR_COUNTRY_DK'), 'calling_code' => '45', 'currency_code' => 'DKK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'DJ' => [ 'name' => Text::_('NR_COUNTRY_DJ'), 'calling_code' => '253', 'currency_code' => 'DJF', 'currency_name' => 'Franc', 'currency_symbol' => 'Fdj' ],
'DM' => [ 'name' => Text::_('NR_COUNTRY_DM'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'DO' => [ 'name' => Text::_('NR_COUNTRY_DO'), 'calling_code' => '1', 'currency_code' => 'DOP', 'currency_name' => 'Peso', 'currency_symbol' => 'RD$' ],
'TL' => [ 'name' => Text::_('NR_COUNTRY_TL'), 'calling_code' => '670', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'EC' => [ 'name' => Text::_('NR_COUNTRY_EC'), 'calling_code' => '593', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'EG' => [ 'name' => Text::_('NR_COUNTRY_EG'), 'calling_code' => '20', 'currency_code' => 'EGP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'SV' => [ 'name' => Text::_('NR_COUNTRY_SV'), 'calling_code' => '503', 'currency_code' => 'SVC', 'currency_name' => 'Colone', 'currency_symbol' => '$' ],
'GQ' => [ 'name' => Text::_('NR_COUNTRY_GQ'), 'calling_code' => '240', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCF' ],
'ER' => [ 'name' => Text::_('NR_COUNTRY_ER'), 'calling_code' => '291', 'currency_code' => 'ERN', 'currency_name' => 'Nakfa', 'currency_symbol' => 'Nfk' ],
'EE' => [ 'name' => Text::_('NR_COUNTRY_EE'), 'calling_code' => '372', 'currency_code' => 'EEK', 'currency_name' => 'Kroon', 'currency_symbol' => 'kr' ],
'ET' => [ 'name' => Text::_('NR_COUNTRY_ET'), 'calling_code' => '251', 'currency_code' => 'ETB', 'currency_name' => 'Birr', 'currency_symbol' => 'Br' ],
'FK' => [ 'name' => Text::_('NR_COUNTRY_FK'), 'calling_code' => '500', 'currency_code' => 'FKP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'FO' => [ 'name' => Text::_('NR_COUNTRY_FO'), 'calling_code' => '298', 'currency_code' => 'DKK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'FJ' => [ 'name' => Text::_('NR_COUNTRY_FJ'), 'calling_code' => '679', 'currency_code' => 'FJD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'FI' => [ 'name' => Text::_('NR_COUNTRY_FI'), 'calling_code' => '358', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'FR' => [ 'name' => Text::_('NR_COUNTRY_FR'), 'calling_code' => '33', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'GF' => [ 'name' => Text::_('NR_COUNTRY_GF'), 'calling_code' => '594', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'PF' => [ 'name' => Text::_('NR_COUNTRY_PF'), 'calling_code' => '689', 'currency_code' => 'XPF', 'currency_name' => 'Franc', 'currency_symbol' => 'F' ],
'TF' => [ 'name' => Text::_('NR_COUNTRY_TF'), 'calling_code' => '262', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'GA' => [ 'name' => Text::_('NR_COUNTRY_GA'), 'calling_code' => '241', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCF' ],
'GM' => [ 'name' => Text::_('NR_COUNTRY_GM'), 'calling_code' => '220', 'currency_code' => 'GMD', 'currency_name' => 'Dalasi', 'currency_symbol' => 'D' ],
'GE' => [ 'name' => Text::_('NR_COUNTRY_GE'), 'calling_code' => '995', 'currency_code' => 'GEL', 'currency_name' => 'Lari', 'currency_symbol' => '₾' ],
'DE' => [ 'name' => Text::_('NR_COUNTRY_DE'), 'calling_code' => '49', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'GH' => [ 'name' => Text::_('NR_COUNTRY_GH'), 'calling_code' => '233', 'currency_code' => 'GHC', 'currency_name' => 'Cedi', 'currency_symbol' => '¢' ],
'GI' => [ 'name' => Text::_('NR_COUNTRY_GI'), 'calling_code' => '350', 'currency_code' => 'GIP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'GR' => [ 'name' => Text::_('NR_COUNTRY_GR'), 'calling_code' => '30', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'GL' => [ 'name' => Text::_('NR_COUNTRY_GL'), 'calling_code' => '299', 'currency_code' => 'DKK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'GD' => [ 'name' => Text::_('NR_COUNTRY_GD'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'GP' => [ 'name' => Text::_('NR_COUNTRY_GP'), 'calling_code' => '590', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'GU' => [ 'name' => Text::_('NR_COUNTRY_GU'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'GT' => [ 'name' => Text::_('NR_COUNTRY_GT'), 'calling_code' => '502', 'currency_code' => 'GTQ', 'currency_name' => 'Quetzal', 'currency_symbol' => 'Q' ],
'GN' => [ 'name' => Text::_('NR_COUNTRY_GN'), 'calling_code' => '224', 'currency_code' => 'GNF', 'currency_name' => 'Franc', 'currency_symbol' => 'FG' ],
'GW' => [ 'name' => Text::_('NR_COUNTRY_GW'), 'calling_code' => '245', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'GY' => [ 'name' => Text::_('NR_COUNTRY_GY'), 'calling_code' => '592', 'currency_code' => 'GYD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'HT' => [ 'name' => Text::_('NR_COUNTRY_HT'), 'calling_code' => '509', 'currency_code' => 'HTG', 'currency_name' => 'Gourde', 'currency_symbol' => 'G' ],
'HM' => [ 'name' => Text::_('NR_COUNTRY_HM'), 'calling_code' => '0', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'HN' => [ 'name' => Text::_('NR_COUNTRY_HN'), 'calling_code' => '504', 'currency_code' => 'HNL', 'currency_name' => 'Lempira', 'currency_symbol' => 'L' ],
'HK' => [ 'name' => Text::_('NR_COUNTRY_HK'), 'calling_code' => '852', 'currency_code' => 'HKD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'HU' => [ 'name' => Text::_('NR_COUNTRY_HU'), 'calling_code' => '36', 'currency_code' => 'HUF', 'currency_name' => 'Forint', 'currency_symbol' => 'Ft' ],
'IS' => [ 'name' => Text::_('NR_COUNTRY_IS'), 'calling_code' => '354', 'currency_code' => 'ISK', 'currency_name' => 'Krona', 'currency_symbol' => 'kr' ],
'IN' => [ 'name' => Text::_('NR_COUNTRY_IN'), 'calling_code' => '91', 'currency_code' => 'INR', 'currency_name' => 'Rupee', 'currency_symbol' => '₹' ],
'ID' => [ 'name' => Text::_('NR_COUNTRY_ID'), 'calling_code' => '62', 'currency_code' => 'IDR', 'currency_name' => 'Rupiah', 'currency_symbol' => 'Rp' ],
'IR' => [ 'name' => Text::_('NR_COUNTRY_IR'), 'calling_code' => '98', 'currency_code' => 'IRR', 'currency_name' => 'Rial', 'currency_symbol' => '﷼' ],
'IQ' => [ 'name' => Text::_('NR_COUNTRY_IQ'), 'calling_code' => '964', 'currency_code' => 'IQD', 'currency_name' => 'Dinar', 'currency_symbol' => 'د.ع' ],
'IE' => [ 'name' => Text::_('NR_COUNTRY_IE'), 'calling_code' => '353', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'IM' => [ 'name' => Text::_('NR_COUNTRY_IM'), 'calling_code' => '44', 'currency_code' => 'GBP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'IL' => [ 'name' => Text::_('NR_COUNTRY_IL'), 'calling_code' => '972', 'currency_code' => 'ILS', 'currency_name' => 'Shekel', 'currency_symbol' => '₪' ],
'IT' => [ 'name' => Text::_('NR_COUNTRY_IT'), 'calling_code' => '39', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'CI' => [ 'name' => Text::_('NR_COUNTRY_CI'), 'calling_code' => '225', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'JM' => [ 'name' => Text::_('NR_COUNTRY_JM'), 'calling_code' => '1', 'currency_code' => 'JMD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'JP' => [ 'name' => Text::_('NR_COUNTRY_JP'), 'calling_code' => '81', 'currency_code' => 'JPY', 'currency_name' => 'Yen', 'currency_symbol' => '¥' ],
'JO' => [ 'name' => Text::_('NR_COUNTRY_JO'), 'calling_code' => '962', 'currency_code' => 'JOD', 'currency_name' => 'Dinar', 'currency_symbol' => 'د.أ' ],
'KZ' => [ 'name' => Text::_('NR_COUNTRY_KZ'), 'calling_code' => '7', 'currency_code' => 'KZT', 'currency_name' => 'Tenge', 'currency_symbol' => 'лв' ],
'KE' => [ 'name' => Text::_('NR_COUNTRY_KE'), 'calling_code' => '254', 'currency_code' => 'KES', 'currency_name' => 'Shilling', 'currency_symbol' => 'KSh' ],
'KI' => [ 'name' => Text::_('NR_COUNTRY_KI'), 'calling_code' => '686', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'KW' => [ 'name' => Text::_('NR_COUNTRY_KW'), 'calling_code' => '965', 'currency_code' => 'KWD', 'currency_name' => 'Dinar', 'currency_symbol' => 'د.ك' ],
'KG' => [ 'name' => Text::_('NR_COUNTRY_KG'), 'calling_code' => '996', 'currency_code' => 'KGS', 'currency_name' => 'Som', 'currency_symbol' => 'лв' ],
'LA' => [ 'name' => Text::_('NR_COUNTRY_LA'), 'calling_code' => '856', 'currency_code' => 'LAK', 'currency_name' => 'Kip', 'currency_symbol' => '₭' ],
'LV' => [ 'name' => Text::_('NR_COUNTRY_LV'), 'calling_code' => '371', 'currency_code' => 'LVL', 'currency_name' => 'Lat', 'currency_symbol' => 'Ls' ],
'LB' => [ 'name' => Text::_('NR_COUNTRY_LB'), 'calling_code' => '961', 'currency_code' => 'LBP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'LS' => [ 'name' => Text::_('NR_COUNTRY_LS'), 'calling_code' => '266', 'currency_code' => 'LSL', 'currency_name' => 'Loti', 'currency_symbol' => 'L' ],
'LR' => [ 'name' => Text::_('NR_COUNTRY_LR'), 'calling_code' => '231', 'currency_code' => 'LRD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'LY' => [ 'name' => Text::_('NR_COUNTRY_LY'), 'calling_code' => '218', 'currency_code' => 'LYD', 'currency_name' => 'Dinar', 'currency_symbol' => 'ل.د' ],
'LI' => [ 'name' => Text::_('NR_COUNTRY_LI'), 'calling_code' => '423', 'currency_code' => 'CHF', 'currency_name' => 'Franc', 'currency_symbol' => 'CHF' ],
'LT' => [ 'name' => Text::_('NR_COUNTRY_LT'), 'calling_code' => '370', 'currency_code' => 'LTL', 'currency_name' => 'Litas', 'currency_symbol' => 'Lt' ],
'LU' => [ 'name' => Text::_('NR_COUNTRY_LU'), 'calling_code' => '352', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MO' => [ 'name' => Text::_('NR_COUNTRY_MO'), 'calling_code' => '853', 'currency_code' => 'MOP', 'currency_name' => 'Pataca', 'currency_symbol' => 'MOP' ],
'MK' => [ 'name' => Text::_('NR_COUNTRY_MK'), 'calling_code' => '389', 'currency_code' => 'MKD', 'currency_name' => 'Denar', 'currency_symbol' => 'ден' ],
'MG' => [ 'name' => Text::_('NR_COUNTRY_MG'), 'calling_code' => '261', 'currency_code' => 'MGA', 'currency_name' => 'Ariary', 'currency_symbol' => 'Ar' ],
'MW' => [ 'name' => Text::_('NR_COUNTRY_MW'), 'calling_code' => '265', 'currency_code' => 'MWK', 'currency_name' => 'Kwacha', 'currency_symbol' => 'MK' ],
'MY' => [ 'name' => Text::_('NR_COUNTRY_MY'), 'calling_code' => '60', 'currency_code' => 'MYR', 'currency_name' => 'Ringgit', 'currency_symbol' => 'RM' ],
'MV' => [ 'name' => Text::_('NR_COUNTRY_MV'), 'calling_code' => '960', 'currency_code' => 'MVR', 'currency_name' => 'Rufiyaa', 'currency_symbol' => 'Rf' ],
'ML' => [ 'name' => Text::_('NR_COUNTRY_ML'), 'calling_code' => '223', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'MT' => [ 'name' => Text::_('NR_COUNTRY_MT'), 'calling_code' => '356', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MH' => [ 'name' => Text::_('NR_COUNTRY_MH'), 'calling_code' => '692', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'MQ' => [ 'name' => Text::_('NR_COUNTRY_MQ'), 'calling_code' => '596', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MR' => [ 'name' => Text::_('NR_COUNTRY_MR'), 'calling_code' => '222', 'currency_code' => 'MRO', 'currency_name' => 'Ouguiya', 'currency_symbol' => 'UM' ],
'MU' => [ 'name' => Text::_('NR_COUNTRY_MU'), 'calling_code' => '230', 'currency_code' => 'MUR', 'currency_name' => 'Rupee', 'currency_symbol' => '₨' ],
'YT' => [ 'name' => Text::_('NR_COUNTRY_YT'), 'calling_code' => '262', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MX' => [ 'name' => Text::_('NR_COUNTRY_MX'), 'calling_code' => '52', 'currency_code' => 'MXN', 'currency_name' => 'Peso', 'currency_symbol' => '$' ],
'FM' => [ 'name' => Text::_('NR_COUNTRY_FM'), 'calling_code' => '691', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'MD' => [ 'name' => Text::_('NR_COUNTRY_MD'), 'calling_code' => '373', 'currency_code' => 'MDL', 'currency_name' => 'Leu', 'currency_symbol' => 'L' ],
'MC' => [ 'name' => Text::_('NR_COUNTRY_MC'), 'calling_code' => '377', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MN' => [ 'name' => Text::_('NR_COUNTRY_MN'), 'calling_code' => '976', 'currency_code' => 'MNT', 'currency_name' => 'Tugrik', 'currency_symbol' => '₮' ],
'ME' => [ 'name' => Text::_('NR_COUNTRY_ME'), 'calling_code' => '382', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'MS' => [ 'name' => Text::_('NR_COUNTRY_MS'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'MA' => [ 'name' => Text::_('NR_COUNTRY_MA'), 'calling_code' => '212', 'currency_code' => 'MAD', 'currency_name' => 'Dirham', 'currency_symbol' => 'DH' ],
'MZ' => [ 'name' => Text::_('NR_COUNTRY_MZ'), 'calling_code' => '258', 'currency_code' => 'MZN', 'currency_name' => 'Meticail', 'currency_symbol' => 'MT' ],
'MM' => [ 'name' => Text::_('NR_COUNTRY_MM'), 'calling_code' => '95', 'currency_code' => 'MMK', 'currency_name' => 'Kyat', 'currency_symbol' => 'K' ],
'NA' => [ 'name' => Text::_('NR_COUNTRY_NA'), 'calling_code' => '264', 'currency_code' => 'NAD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'NR' => [ 'name' => Text::_('NR_COUNTRY_NR'), 'calling_code' => '674', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'NP' => [ 'name' => Text::_('NR_COUNTRY_NP'), 'calling_code' => '977', 'currency_code' => 'NPR', 'currency_name' => 'Rupee', 'currency_symbol' => '₨' ],
'NL' => [ 'name' => Text::_('NR_COUNTRY_NL'), 'calling_code' => '31', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'NC' => [ 'name' => Text::_('NR_COUNTRY_NC'), 'calling_code' => '687', 'currency_code' => 'XPF', 'currency_name' => 'Franc', 'currency_symbol' => 'F' ],
'NZ' => [ 'name' => Text::_('NR_COUNTRY_NZ'), 'calling_code' => '64', 'currency_code' => 'NZD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'NI' => [ 'name' => Text::_('NR_COUNTRY_NI'), 'calling_code' => '505', 'currency_code' => 'NIO', 'currency_name' => 'Cordoba', 'currency_symbol' => 'C$' ],
'NE' => [ 'name' => Text::_('NR_COUNTRY_NE'), 'calling_code' => '227', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'NG' => [ 'name' => Text::_('NR_COUNTRY_NG'), 'calling_code' => '234', 'currency_code' => 'NGN', 'currency_name' => 'Naira', 'currency_symbol' => '₦' ],
'NU' => [ 'name' => Text::_('NR_COUNTRY_NU'), 'calling_code' => '683', 'currency_code' => 'NZD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'NF' => [ 'name' => Text::_('NR_COUNTRY_NF'), 'calling_code' => '672', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'KP' => [ 'name' => Text::_('NR_COUNTRY_KP'), 'calling_code' => '850', 'currency_code' => 'KPW', 'currency_name' => 'Won', 'currency_symbol' => '₩' ],
'MP' => [ 'name' => Text::_('NR_COUNTRY_MP'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'NO' => [ 'name' => Text::_('NR_COUNTRY_NO'), 'calling_code' => '47', 'currency_code' => 'NOK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'OM' => [ 'name' => Text::_('NR_COUNTRY_OM'), 'calling_code' => '968', 'currency_code' => 'OMR', 'currency_name' => 'Rial', 'currency_symbol' => '﷼' ],
'PK' => [ 'name' => Text::_('NR_COUNTRY_PK'), 'calling_code' => '92', 'currency_code' => 'PKR', 'currency_name' => 'Rupee', 'currency_symbol' => '₨' ],
'PW' => [ 'name' => Text::_('NR_COUNTRY_PW'), 'calling_code' => '680', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'PS' => [ 'name' => Text::_('NR_COUNTRY_PS'), 'calling_code' => '970', 'currency_code' => 'ILS', 'currency_name' => 'Shekel', 'currency_symbol' => '₪' ],
'PA' => [ 'name' => Text::_('NR_COUNTRY_PA'), 'calling_code' => '507', 'currency_code' => 'PAB', 'currency_name' => 'Balboa', 'currency_symbol' => 'B/.' ],
'PG' => [ 'name' => Text::_('NR_COUNTRY_PG'), 'calling_code' => '675', 'currency_code' => 'PGK', 'currency_name' => 'Kina', 'currency_symbol' => 'K' ],
'PY' => [ 'name' => Text::_('NR_COUNTRY_PY'), 'calling_code' => '595', 'currency_code' => 'PYG', 'currency_name' => 'Guarani', 'currency_symbol' => 'Gs' ],
'PE' => [ 'name' => Text::_('NR_COUNTRY_PE'), 'calling_code' => '51', 'currency_code' => 'PEN', 'currency_name' => 'Sol', 'currency_symbol' => 'S/.' ],
'PH' => [ 'name' => Text::_('NR_COUNTRY_PH'), 'calling_code' => '63', 'currency_code' => 'PHP', 'currency_name' => 'Peso', 'currency_symbol' => 'Php' ],
'PN' => [ 'name' => Text::_('NR_COUNTRY_PN'), 'calling_code' => '870', 'currency_code' => 'NZD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'PL' => [ 'name' => Text::_('NR_COUNTRY_PL'), 'calling_code' => '48', 'currency_code' => 'PLN', 'currency_name' => 'Zloty', 'currency_symbol' => 'zł' ],
'PT' => [ 'name' => Text::_('NR_COUNTRY_PT'), 'calling_code' => '351', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'PR' => [ 'name' => Text::_('NR_COUNTRY_PR'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'QA' => [ 'name' => Text::_('NR_COUNTRY_QA'), 'calling_code' => '974', 'currency_code' => 'QAR', 'currency_name' => 'Rial', 'currency_symbol' => '﷼' ],
'CG' => [ 'name' => Text::_('NR_COUNTRY_CG'), 'calling_code' => '242', 'currency_code' => 'XAF', 'currency_name' => 'Franc', 'currency_symbol' => 'FCF' ],
'RE' => [ 'name' => Text::_('NR_COUNTRY_RE'), 'calling_code' => '262', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'RO' => [ 'name' => Text::_('NR_COUNTRY_RO'), 'calling_code' => '40', 'currency_code' => 'RON', 'currency_name' => 'Leu', 'currency_symbol' => 'lei' ],
'RU' => [ 'name' => Text::_('NR_COUNTRY_RU'), 'calling_code' => '7', 'currency_code' => 'RUB', 'currency_name' => 'Ruble', 'currency_symbol' => 'руб' ],
'RW' => [ 'name' => Text::_('NR_COUNTRY_RW'), 'calling_code' => '250', 'currency_code' => 'RWF', 'currency_name' => 'Franc', 'currency_symbol' => 'FRw' ],
'SH' => [ 'name' => Text::_('NR_COUNTRY_SH'), 'calling_code' => '290', 'currency_code' => 'SHP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'KN' => [ 'name' => Text::_('NR_COUNTRY_KN'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'LC' => [ 'name' => Text::_('NR_COUNTRY_LC'), 'calling_code' => '1758', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'PM' => [ 'name' => Text::_('NR_COUNTRY_PM'), 'calling_code' => '508', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'VC' => [ 'name' => Text::_('NR_COUNTRY_VC'), 'calling_code' => '1', 'currency_code' => 'XCD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'WS' => [ 'name' => Text::_('NR_COUNTRY_WS'), 'calling_code' => '685', 'currency_code' => 'WST', 'currency_name' => 'Tala', 'currency_symbol' => 'WS$' ],
'SM' => [ 'name' => Text::_('NR_COUNTRY_SM'), 'calling_code' => '378', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'ST' => [ 'name' => Text::_('NR_COUNTRY_ST'), 'calling_code' => '239', 'currency_code' => 'STD', 'currency_name' => 'Dobra', 'currency_symbol' => 'Db' ],
'SA' => [ 'name' => Text::_('NR_COUNTRY_SA'), 'calling_code' => '966', 'currency_code' => 'SAR', 'currency_name' => 'Rial', 'currency_symbol' => '﷼' ],
'SN' => [ 'name' => Text::_('NR_COUNTRY_SN'), 'calling_code' => '221', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'RS' => [ 'name' => Text::_('NR_COUNTRY_RS'), 'calling_code' => '381', 'currency_code' => 'RSD', 'currency_name' => 'Dinar', 'currency_symbol' => 'Дин' ],
'SC' => [ 'name' => Text::_('NR_COUNTRY_SC'), 'calling_code' => '248', 'currency_code' => 'SCR', 'currency_name' => 'Rupee', 'currency_symbol' => '₨' ],
'SL' => [ 'name' => Text::_('NR_COUNTRY_SL'), 'calling_code' => '232', 'currency_code' => 'SLL', 'currency_name' => 'Leone', 'currency_symbol' => 'Le' ],
'SG' => [ 'name' => Text::_('NR_COUNTRY_SG'), 'calling_code' => '65', 'currency_code' => 'SGD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'SK' => [ 'name' => Text::_('NR_COUNTRY_SK'), 'calling_code' => '421', 'currency_code' => 'SKK', 'currency_name' => 'Koruna', 'currency_symbol' => 'Sk' ],
'SI' => [ 'name' => Text::_('NR_COUNTRY_SI'), 'calling_code' => '386', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'SB' => [ 'name' => Text::_('NR_COUNTRY_SB'), 'calling_code' => '677', 'currency_code' => 'SBD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'SO' => [ 'name' => Text::_('NR_COUNTRY_SO'), 'calling_code' => '252', 'currency_code' => 'SOS', 'currency_name' => 'Shilling', 'currency_symbol' => 'S' ],
'ZA' => [ 'name' => Text::_('NR_COUNTRY_ZA'), 'calling_code' => '27', 'currency_code' => 'ZAR', 'currency_name' => 'Rand', 'currency_symbol' => 'R' ],
'GS' => [ 'name' => Text::_('NR_COUNTRY_GS'), 'calling_code' => '500', 'currency_code' => 'GBP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'KR' => [ 'name' => Text::_('NR_COUNTRY_KR'), 'calling_code' => '82', 'currency_code' => 'KRW', 'currency_name' => 'Won', 'currency_symbol' => '₩' ],
'ES' => [ 'name' => Text::_('NR_COUNTRY_ES'), 'calling_code' => '34', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'LK' => [ 'name' => Text::_('NR_COUNTRY_LK'), 'calling_code' => '94', 'currency_code' => 'LKR', 'currency_name' => 'Rupee', 'currency_symbol' => '₨' ],
'SD' => [ 'name' => Text::_('NR_COUNTRY_SD'), 'calling_code' => '249', 'currency_code' => 'SDD', 'currency_name' => 'Dinar', 'currency_symbol' => 'ج.س' ],
'SS' => [ 'name' => Text::_('NR_COUNTRY_SS'), 'calling_code' => '211', 'currency_code' => 'SSP', 'currency_name' => 'Pound', 'currency_symbol' => 'SS£' ],
'SR' => [ 'name' => Text::_('NR_COUNTRY_SR'), 'calling_code' => '597', 'currency_code' => 'SRD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'SJ' => [ 'name' => Text::_('NR_COUNTRY_SJ'), 'calling_code' => '47', 'currency_code' => 'NOK', 'currency_name' => 'Krone', 'currency_symbol' => 'kr' ],
'SZ' => [ 'name' => Text::_('NR_COUNTRY_SZ'), 'calling_code' => '268', 'currency_code' => 'SZL', 'currency_name' => 'Lilangeni', 'currency_symbol' => 'L' ],
'SE' => [ 'name' => Text::_('NR_COUNTRY_SE'), 'calling_code' => '46', 'currency_code' => 'SEK', 'currency_name' => 'Krona', 'currency_symbol' => 'kr' ],
'CH' => [ 'name' => Text::_('NR_COUNTRY_CH'), 'calling_code' => '41', 'currency_code' => 'CHF', 'currency_name' => 'Franc', 'currency_symbol' => 'CHF' ],
'SY' => [ 'name' => Text::_('NR_COUNTRY_SY'), 'calling_code' => '963', 'currency_code' => 'SYP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'TW' => [ 'name' => Text::_('NR_COUNTRY_TW'), 'calling_code' => '886', 'currency_code' => 'TWD', 'currency_name' => 'Dollar', 'currency_symbol' => 'NT$' ],
'TJ' => [ 'name' => Text::_('NR_COUNTRY_TJ'), 'calling_code' => '992', 'currency_code' => 'TJS', 'currency_name' => 'Somoni', 'currency_symbol' => 'SM' ],
'TZ' => [ 'name' => Text::_('NR_COUNTRY_TZ'), 'calling_code' => '255', 'currency_code' => 'TZS', 'currency_name' => 'Shilling', 'currency_symbol' => 'TSh' ],
'TH' => [ 'name' => Text::_('NR_COUNTRY_TH'), 'calling_code' => '66', 'currency_code' => 'THB', 'currency_name' => 'Baht', 'currency_symbol' => '฿' ],
'TG' => [ 'name' => Text::_('NR_COUNTRY_TG'), 'calling_code' => '228', 'currency_code' => 'XOF', 'currency_name' => 'Franc', 'currency_symbol' => 'CFA' ],
'TK' => [ 'name' => Text::_('NR_COUNTRY_TK'), 'calling_code' => '690', 'currency_code' => 'NZD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'TO' => [ 'name' => Text::_('NR_COUNTRY_TO'), 'calling_code' => '676', 'currency_code' => 'TOP', 'currency_name' => 'Paanga', 'currency_symbol' => 'T$' ],
'TT' => [ 'name' => Text::_('NR_COUNTRY_TT'), 'calling_code' => '1', 'currency_code' => 'TTD', 'currency_name' => 'Dollar', 'currency_symbol' => 'TT$' ],
'TN' => [ 'name' => Text::_('NR_COUNTRY_TN'), 'calling_code' => '216', 'currency_code' => 'TND', 'currency_name' => 'Dinar', 'currency_symbol' => 'د.ت' ],
'TR' => [ 'name' => Text::_('NR_COUNTRY_TR'), 'calling_code' => '90', 'currency_code' => 'TRY', 'currency_name' => 'Lira', 'currency_symbol' => 'YTL' ],
'TM' => [ 'name' => Text::_('NR_COUNTRY_TM'), 'calling_code' => '993', 'currency_code' => 'TMM', 'currency_name' => 'Manat', 'currency_symbol' => 'm' ],
'TC' => [ 'name' => Text::_('NR_COUNTRY_TC'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'TV' => [ 'name' => Text::_('NR_COUNTRY_TV'), 'calling_code' => '688', 'currency_code' => 'AUD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'VI' => [ 'name' => Text::_('NR_COUNTRY_VI'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'UG' => [ 'name' => Text::_('NR_COUNTRY_UG'), 'calling_code' => '256', 'currency_code' => 'UGX', 'currency_name' => 'Shilling', 'currency_symbol' => 'USh' ],
'UA' => [ 'name' => Text::_('NR_COUNTRY_UA'), 'calling_code' => '380', 'currency_code' => 'UAH', 'currency_name' => 'Hryvnia', 'currency_symbol' => '₴' ],
'AE' => [ 'name' => Text::_('NR_COUNTRY_AE'), 'calling_code' => '971', 'currency_code' => 'AED', 'currency_name' => 'Dirham', 'currency_symbol' => 'د.إ' ],
'GB' => [ 'name' => Text::_('NR_COUNTRY_GB'), 'calling_code' => '44', 'currency_code' => 'GBP', 'currency_name' => 'Pound', 'currency_symbol' => '£' ],
'US' => [ 'name' => Text::_('NR_COUNTRY_US'), 'calling_code' => '1', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'UM' => [ 'name' => Text::_('NR_COUNTRY_UM'), 'calling_code' => '246', 'currency_code' => 'USD', 'currency_name' => 'Dollar', 'currency_symbol' => '$' ],
'UY' => [ 'name' => Text::_('NR_COUNTRY_UY'), 'calling_code' => '598', 'currency_code' => 'UYU', 'currency_name' => 'Peso', 'currency_symbol' => '$U' ],
'UZ' => [ 'name' => Text::_('NR_COUNTRY_UZ'), 'calling_code' => '998', 'currency_code' => 'UZS', 'currency_name' => 'Som', 'currency_symbol' => 'лв' ],
'VU' => [ 'name' => Text::_('NR_COUNTRY_VU'), 'calling_code' => '678', 'currency_code' => 'VUV', 'currency_name' => 'Vatu', 'currency_symbol' => 'Vt' ],
'VA' => [ 'name' => Text::_('NR_COUNTRY_VA'), 'calling_code' => '39', 'currency_code' => 'EUR', 'currency_name' => 'Euro', 'currency_symbol' => '€' ],
'VE' => [ 'name' => Text::_('NR_COUNTRY_VE'), 'calling_code' => '58', 'currency_code' => 'VEF', 'currency_name' => 'Bolivar', 'currency_symbol' => 'Bs' ],
'VN' => [ 'name' => Text::_('NR_COUNTRY_VN'), 'calling_code' => '84', 'currency_code' => 'VND', 'currency_name' => 'Dong', 'currency_symbol' => '₫' ],
'WF' => [ 'name' => Text::_('NR_COUNTRY_WF'), 'calling_code' => '681', 'currency_code' => 'XPF', 'currency_name' => 'Franc', 'currency_symbol' => 'F' ],
'EH' => [ 'name' => Text::_('NR_COUNTRY_EH'), 'calling_code' => '212', 'currency_code' => 'MAD', 'currency_name' => 'Dirham', 'currency_symbol' => 'DH' ],
'YE' => [ 'name' => Text::_('NR_COUNTRY_YE'), 'calling_code' => '967', 'currency_code' => 'YER', 'currency_name' => 'Rial', 'currency_symbol' => '﷼' ],
'ZM' => [ 'name' => Text::_('NR_COUNTRY_ZM'), 'calling_code' => '260', 'currency_code' => 'ZMK', 'currency_name' => 'Kwacha', 'currency_symbol' => 'ZK' ],
'ZW' => [ 'name' => Text::_('NR_COUNTRY_ZW'), 'calling_code' => '263', 'currency_code' => 'ZWD', 'currency_name' => 'Dollar', 'currency_symbol' => 'Z$' ]
];
// Sort by name
uasort($list, function ($item1, $item2)
{
return $item1['name'] <=> $item2['name'];
});
return $list;
}
} Email.php 0000644 00000020041 15235314576 0006314 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die('Restricted access');
use NRFramework\Functions;
use NRFramework\URLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\Filesystem\Path;
use Joomla\CMS\Language\Text;
/**
* Framework Emailer
*/
class Email
{
/**
* Indicates the last error
*
* @var string
*/
public $error;
/**
* Email Object
*
* @var email data to be sent
*/
private $email;
/**
* Required elements for a valid email object
*
* @var array
*/
private $requiredKeys = [
'from_email',
'from_name',
'recipient',
'subject',
'body'
];
/**
* Class constructor
*/
public function __construct($email)
{
$this->email = $email;
}
/**
* Validates Email Object
*
* @param array $email The email object
*
* @return boolean Returns true if the email object is valid
*/
public function validate()
{
// Validate email object
if (!$this->email || !is_array($this->email) || !count($this->email))
{
$this->setError('Invalid email object.');
return;
}
// Check for missing properties
foreach ($this->requiredKeys as $key)
{
if (!isset($this->email[$key]) || empty($this->email[$key]))
{
$this->setError("The $key field is either missing or invalid.");
return;
}
}
// Validate recipient email addresses.
$this->email['recipient'] = Functions::makeArray($this->email['recipient']);
foreach ($this->email['recipient'] as $recipient)
{
if (!$this->validateEmailAddress($recipient))
{
$this->setError("Invalid recipient email address: $recipient");
return;
}
}
// Validate sender email address
if (!$this->validateEmailAddress($this->email['from_email']))
{
$this->setError('Invalid sender email address: ' . $this->email['from_email']);
return;
}
$this->email['bcc'] = isset($this->email['bcc']) ? Functions::makeArray($this->email['bcc']) : [];
$this->email['cc'] = isset($this->email['cc']) ? Functions::makeArray($this->email['cc']) : [];
// Convert special HTML entities back to characters on non text-only properties.
// For instance, the subject line of an email is not parsed as HTML, it's just pure text.
// Because of this an HTML entity like & it will be displayed as encoded.
// To prevent this from happening we need decode the values.
$this->email['subject'] = htmlspecialchars_decode($this->email['subject']);
$this->email['from_name'] = htmlspecialchars_decode($this->email['from_name']);
$this->email['reply_to_name'] = htmlspecialchars_decode($this->email['reply_to_name']);
return true;
}
/**
* Sending emails
*
* @param array $email The mail objecta
*
* @return mixed Returns true on success. Throws exeption on fail.
*/
public function send()
{
// Proceed only if Mail Sending is enabled.
if (!Factory::getConfig()->get('mailonline'))
{
$this->error = Text::_('NR_ERROR_EMAIL_IS_DISABLED');
return;
}
// Validate first the email object
if (!$this->validate($this->email))
{
return;
}
$email = $this->email;
$mailer = Factory::getMailer();
$mailer->CharSet = 'UTF-8';
$mailer->Encoding = 'quoted-printable';
// Email Sender
$mailer->setSender([
$email['from_email'],
$email['from_name']
]);
// Reply-to
if (isset($email['reply_to']) && !empty($email['reply_to']))
{
$name = (isset($email['reply_to_name']) && !empty($email['reply_to_name'])) ? $email['reply_to_name'] : '';
$reply_to_addresses = array_filter(array_map('trim', explode(',', $email['reply_to'])));
foreach($reply_to_addresses as $reply_to_address)
{
$mailer->addReplyTo($reply_to_address, $name);
}
}
// Convert all relative paths found in <a> and <img> elements to absolute URLs
$email['body'] = URLHelper::relativePathsToAbsoluteURLs($email['body']);
// Fix space characters displayed as ???? in old email clients like SquirrelMail.
// Ticket reference: https://smilemotive.teamwork.com/desk/tickets/96313487/messages
$specialSpace = [
"\xC2\xA0",
"\xE1\xA0\x8E",
"\xE2\x80\x80",
"\xE2\x80\x81",
"\xE2\x80\x82",
"\xE2\x80\x83",
"\xE2\x80\x84",
"\xE2\x80\x85",
"\xE2\x80\x86",
"\xE2\x80\x87",
"\xE2\x80\x88",
"\xE2\x80\x89",
"\xE2\x80\x8A",
"\xE2\x80\x8B",
"\xE2\x80\xAF",
"\xE2\x81\x9F",
"\xEF\xBB\xBF",
];
$email['body'] = str_replace($specialSpace, " ", $email['body']);
$mailer
->addRecipient($email['recipient'])
->isHTML(true)
->setSubject($email['subject'])
->setBody($email['body']);
$mailer->AltBody = strip_tags(str_ireplace(['<br />', '<br>', '<br/>'], "\r\n", $email['body']));
// Add CC
if (!empty($email['cc']))
{
$mailer->addCc($email['cc']);
}
// Add BCC
if (!empty($email['bcc']))
{
$mailer->addBcc($email['bcc']);
}
// Attachments
$attachments = $email['attachments'];
if (!empty($attachments))
{
if (!is_array($attachments))
{
$attachments = explode(',', $attachments);
}
// Validate Attachments
$attachments = array_filter(array_map('trim', $attachments));
foreach ($attachments as $attachment)
{
$file_path = $this->toRelativePath($attachment);
if (!is_file($file_path))
{
continue;
}
$mailer->addAttachment($file_path);
}
}
// Send mail
$send = $mailer->Send();
if ($send !== true)
{
$this->setError($send->__toString());
return;
}
return true;
}
/**
* Set Class Error
*
* @param string $error The error message
*/
private function setError($error)
{
$this->error = 'Error sending email: ' . $error;
}
/**
* Removes all illegal characters and validates an email address
*
* @param string $email Email address string
*
* @return bool
*/
private function validateEmailAddress($email)
{
// If the email address contains an ampersand, throw an error
if (strpos($email, '&') !== false)
{
return false;
}
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Attempts to transform an absolute URL to path relative to the site's root.
*
* @param string $url
*
* @return string
*/
private function toRelativePath($url)
{
$needles = [
Uri::root(),
JPATH_SITE,
JPATH_ROOT
];
$path = str_replace($needles, '', $url);
$path = Path::clean($path);
// Relative paths should not start with a slash.
$path = ltrim($path, '/');
return $path;
}
} Executer.php 0000644 00000014202 15235314576 0007053 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\Filesystem\File;
use Joomla\CMS\Factory;
/**
* Cleverly evaluate php code using a temporary file and without using the evil eval() PHP method
*/
class Executer
{
/**
* The php code is going to be executed
*
* @var string
*/
private $php_code;
/**
* The data object passed as argument to function
*
* @var mixed
*/
private $payload;
/**
* Executer configuration
*
* @var object
*/
private $options;
/**
* Class constructor
*
* @param string $php_code The php code is going to be executed
*/
public function __construct($php_code = null, &$payload = null, $options = array())
{
$this->setPhpCode($php_code);
$this->setPayload($payload);
// Default options
$defaults = [
'forbidden_php_functions' => [
'fopen',
'popen',
'unlink',
'rmdir',
'dl',
'escapeshellarg',
'escapeshellcmd',
'exec',
'passthru',
'proc_close',
'proc_open',
'shell_exec',
'symlink',
'system',
'pcntl_exec',
'eval',
'create_function'
]
];
$options = array_merge($defaults, $options);
$this->options = new Registry($options);
}
/**
* Payload contains the variables passed as argumentse into the PHP code
*
* @param mixed $data
*
* @return void
*/
public function setPayload(&$data)
{
$this->payload = &$data;
return $this;
}
/**
* Set forbidden PHP functions. If any found, the whole PHP block won't run.
*
* @param array $functions
*
* @return void
*/
public function setForbiddenPHPFunctions($functions)
{
if (empty($functions))
{
return $this;
}
if (is_string($functions))
{
$functions = explode(',', $functions);
}
$this->options->set('forbidden_php_functions', $functions);
return $this;
}
/**
* Helper method to set the php code is about to be executed
*
* @param string $php_code
*
* @return void
*/
public function setPhpCode($php_code)
{
$this->php_code = $php_code;
return $this;
}
/**
* Checks if given PHP code is valid and it's allowed to run.
*
* @return bool
*/
private function allowedToRun()
{
// Check for forbidden PHP functions
$re = '/(' . implode('|', $this->options->get('forbidden_php_functions')) . ')(\s*\(|\s+[\'"])/mi';
preg_match_all($re, $this->php_code ?? '', $matches);
if (!empty($matches[0]))
{
return false;
}
// Check for backticks ``
if ($has_back_ticks = preg_match('/`(.*?)`/s', $this->php_code ?? ''))
{
return false;
}
return true;
}
/**
* Run function
*
* @return function
*/
public function run()
{
if (!$this->allowedToRun())
{
return;
}
$function_name = $this->getFunctionName();
// Function doesn't exist. Let's create it.
if (!function_exists($function_name))
{
if (!$this->createFunction())
{
return;
}
}
return $function_name($this->payload);
}
/**
* Creates a temporary function in memory
*
* @return void
*/
private function createFunction()
{
$function_name = $this->getFunctionName();
$function_content = $this->getFunctionContent();
$temp_file = $this->getTempPath() . '/' . $function_name;
// Write function's content to a temporary file
File::write($temp_file, $function_content);
// Include file
include_once $temp_file;
// Delete file
if (!defined('JDEBUG') || !JDEBUG)
{
@chmod($temp_file, 0777);
@unlink($temp_file);
}
return function_exists($function_name);
}
/**
* Get temporary file content
*
* @return string
*/
private function getFunctionContent()
{
$function_name = $this->getFunctionName();
$variables = $this->getFunctionVariables();
$contents = [
'<?php',
'defined(\'_JEXEC\') or die;',
'function ' . $function_name . '(&$displayData = null) {
if ($displayData) {
extract($displayData, EXTR_REFS);
}
',
implode("\n", $variables),
$this->php_code,
';return true;}'
];
$contents = implode("\n", $contents);
// Remove Zero Width spaces / (non-)joiners
$contents = str_replace(
[
"\xE2\x80\x8B",
"\xE2\x80\x8C",
"\xE2\x80\x8D",
],
'',
$contents
);
return $contents;
}
/**
* Make user's life easier by initializing some Joomla helpful variables
*
* @return array
*/
protected function getFunctionVariables()
{
return [
'$app = $mainframe = \Joomla\CMS\Factory::getApplication();',
'$document = $doc = \Joomla\CMS\Factory::getDocument();',
'$database = $db = \Joomla\CMS\Factory::getDbo();',
'$user = \Joomla\CMS\Factory::getUser();',
'$Itemid = $app->input->getInt(\'Itemid\');'
];
}
/**
* Construct a temporary function name
*
* @return string
*/
private function getFunctionName()
{
return 'tassos_php_' . md5($this->php_code ?? '');
}
/**
* Return Joomla temporary path
*
* @return void
*/
private function getTempPath()
{
return Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
}
} Image.php 0000644 00000047524 15235314576 0006326 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
// No direct access
defined('_JEXEC') or die;
use NRFramework\Mimes;
use NRFramework\File;
use Joomla\CMS\Image\Image as JoomlaImage;
use Joomla\Filesystem\Path;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
class Image
{
/**
* Resize an image.
*
* @param string $source
* @param string $width
* @param string $height
* @param integer $quality
* @param string $mode
* @param boolean $unique_filename
* @param boolean $fix_orientation
* @param string $gif_mode If the uploaded image is a GIF image, how will it be copied? Options: "copy" source, "resize" source
*
* @return mixed
*/
public static function resize($source, $width, $height, $quality = 70, $mode = 'crop', $destination = '', $unique_filename = false, $fix_orientation = true, $gif_mode = 'copy')
{
$width = (int) $width;
$height = (int) $height;
// Destination file name
$destination = empty($destination) ? $source : $destination;
// size must be WIDTHxHEIGHT
$size = $width . 'x' . $height;
switch ($mode)
{
// Crop and Resize
case 'crop':
$mode = 5;
break;
// Scale Fill
case 'stretch':
$mode = 1;
break;
// Fit, will fill empty space with black
case 'fit':
$mode = 6;
break;
default:
$mode = 5;
break;
}
try {
$image = new JoomlaImage($source);
$origWidth = $image->getWidth();
$origHeight = $image->getHeight();
/**
* If the image width is less than the given width,
* set the image width we are resizing to the image's width.
*/
if ($origWidth < $width)
{
$size = $origWidth . 'x';
if ($origHeight < $height)
{
$size .= $origHeight;
}
else
{
$size .= $height;
}
}
else if ($origHeight < $height)
{
$prefix = $width;
if ($origWidth < $width)
{
$prefix = $origWidth;
}
$size = $prefix . 'x' . $origHeight;
}
// Fix orientation
if ($fix_orientation)
{
self::fixOrientation($image);
}
// Determine the MIME of the original file to get the proper type
$mime = Mimes::detectFileType($source);
// PNG images should not have a quality value
$options = $mime == 'image/png' ? ['quality' => 9] : ['quality' => $quality];
// Get the image type
$image_type = self::getImageType($mime);
if ($unique_filename)
{
// Make destination file unique
File::uniquefy($destination);
}
$destination = Path::clean($destination);
// Resize image
if ($mime === 'image/gif')
{
if ($gif_mode === 'copy')
{
File::copy($source, $destination, true);
}
else
{
foreach ($image->generateThumbs($size, $mode) as $thumb)
{
$thumb->toFile($destination, $image_type, $options);
}
}
}
else
{
foreach ($image->generateThumbs($size, $mode) as $thumb)
{
$thumb->toFile($destination, $image_type, $options);
}
}
return $destination;
} catch(\Exception $e) {}
return false;
}
/**
* Resizes an image by height.
*
* @param string $src
* @param string $height
* @param string $destination
* @param int $quality
* @param bool $unique_filename
* @param bool $fix_orientation
* @param string $gif_mode If the uploaded image is a GIF image, how will it be copied? Options: "copy" source, "resize" source
*
* @return bool
*/
public static function resizeByHeight($src, $height, $destination = null, $quality = 70, $unique_filename = false, $fix_orientation = true, $gif_mode = 'copy')
{
$height = (int) $height;
// Create a new JImage object from the source image path
$image = new JoomlaImage($src);
// Fix orientation
if ($fix_orientation)
{
self::fixOrientation($image);
}
// Determine the MIME of the original file to get the proper type
$mime = Mimes::detectFileType($src);
// Get the image type
$image_type = self::getImageType($mime);
// Output file name
$destination = empty($destination) ? $src : $destination;
if ($unique_filename)
{
// Make destination file unique
File::uniquefy($destination);
}
$destination = Path::clean($destination);
// PNG images should not have a quality value
$options = $mime == 'image/png' ? ['quality' => 9] : ['quality' => $quality];
// Get the original width and height of the image
$origWidth = $image->getWidth();
$origHeight = $image->getHeight();
// Calculate the new width based on the desired height
$newWidth = ($origWidth / $origHeight) * $height;
// Resize image
if ($mime === 'image/gif')
{
if ($gif_mode === 'copy')
{
File::copy($source, $destination, true);
}
else
{
$resizedImage = $image->resize($newWidth, $height);
$resizedImage->toFile($destination, $image_type, $options);
}
}
else
{
$resizedImage = $image->resize($newWidth, $height);
$resizedImage->toFile($destination, $image_type, $options);
}
// Return true if the image was successfully resized and saved, false otherwise
return $destination;
}
/**
* Resizes an image by keeping the aspect ratio
*
* @param string $source
* @param array $width
* @param integer $quality
* @param array $destination
* @param boolean $unique_filename
* @param boolean $fix_orientation
* @param string $gif_mode If the uploaded image is a GIF image, how will it be copied? Options: "copy" source, "resize" source
*
* @return boolean
*/
public static function resizeAndKeepAspectRatio($source, $width, $quality = 70, $destination = '', $unique_filename = false, $fix_orientation = true, $gif_mode = 'copy')
{
// Ensure we have received valid image dimensions
if (!count($image_dimensions = getimagesize($source)))
{
return false;
}
// Get the image width
if (!$uploaded_image_width = (int) $image_dimensions[0])
{
return false;
}
// Get the image height
if (!$uploaded_image_height = (int) $image_dimensions[1])
{
return false;
}
$width = (int) $width;
/**
* If the image width is less than the given width,
* set the image width we are resizing to the image's width.
*/
if ($uploaded_image_width < (int) $width)
{
$width = $uploaded_image_width;
}
// Determine the MIME of the original file to get the proper type
$mime = Mimes::detectFileType($source);
// PNG images should not have a quality value
$options = $mime == 'image/png' ? ['quality' => 9] : ['quality' => $quality];
// Get the image type
$image_type = self::getImageType($mime);
try {
// Get image object
$image = new JoomlaImage($source);
// Fix orientation
if ($fix_orientation)
{
self::fixOrientation($image);
}
// Calculate aspect ratio
$ratio = $uploaded_image_width / $uploaded_image_height;
// Get new height based on aspect ratio
$targetHeight = $width / $ratio;
// Output file name
$destination = empty($destination) ? $source : $destination;
if ($unique_filename)
{
// Make destination file unique
File::uniquefy($destination);
}
$destination = Path::clean($destination);
// Resize image
if ($mime === 'image/gif')
{
if ($gif_mode === 'copy')
{
File::copy($source, $destination, true);
}
else
{
$resizedImage = $image->resize($width, $targetHeight, true);
$resizedImage->toFile($destination, $image_type, $options);
}
}
else
{
$resizedImage = $image->resize($width, $targetHeight, true);
$resizedImage->toFile($destination, $image_type, $options);
}
return $destination;
} catch(\Exception $e) {}
return false;
}
public static function resizeByWidthOrHeight($source, $width, $height, $quality = 80, $destination = '', $resize_method = 'crop', $unique_filename = false, $fix_orientation = true)
{
$resized_image = null;
// If width is null, and we have height set, we are resizing by height
if (is_null($width) && $height && !is_null($height))
{
$resized_image = Image::resizeByHeight($source, $height, $destination, 80, $unique_filename, $fix_orientation);
}
else
{
/**
* If height is zero, then we suppose we want to keep aspect ratio.
*
* Resize with width & height: If height is not set
* Resize and keep aspect ratio: If height is set
*/
$resized_image = $height && !is_null($height)
?
Image::resize($source, $width, $height, 80, $resize_method, $destination, $unique_filename, $fix_orientation)
:
Image::resizeAndKeepAspectRatio($source, $width, 80, $destination, $unique_filename, $fix_orientation);
}
return $resized_image;
}
/**
* Returns the orientation of the image.
*
* @param string $path
*
* @return int
*/
public static function getOrientation($path)
{
if (!$exif = @exif_read_data($path))
{
return;
}
return intval(@$exif['Orientation']);
}
/**
* Fixes the orientation of the generated image and ensures it appears with the same orientation as the source.
*
* @param string $path
* @param int $orientation
*
* @return void
*/
public static function fixOrientation(&$image, $orientation = null)
{
$orientation = self::getOrientation($image->getPath());
if(!in_array($orientation, [3, 6, 8]))
{
return;
}
switch ($orientation)
{
case 3:
$image->rotate(180, -1, false);
break;
case 6:
$image->rotate(270, -1, false);
break;
case 8:
$image->rotate(90, -1, false);
break;
}
return true;
}
/**
* Returns the image type based on its mime type
*
* @param string $mime
*
* @return int
*/
public static function getImageType($mime)
{
switch ($mime)
{
case 'image/png':
return IMAGETYPE_PNG;
break;
case 'image/gif':
return IMAGETYPE_GIF;
break;
case 'image/webp':
return IMAGETYPE_WEBP;
break;
case 'image/jpeg':
default:
return IMAGETYPE_JPEG;
break;
}
}
/**
* Creates a watermark from text.
*
* @param string $text
* @param integer $font_size
* @param integer $opacity
* @param string $color
* @param integer $originalWidth
* @param integer $originalHeight
*
* @return object
*/
public static function createWatermarkText($text = '', $_font_size = 30, $opacity = 60, $color = '#ffffff', $originalWidth = null, $originalHeight = null)
{
$font = implode(DIRECTORY_SEPARATOR, [JPATH_SITE, 'media', 'plg_system_nrframework', 'font', 'arial.ttf']);
// Scale down font size based on the original image width or height
$dimension = $originalWidth > $originalHeight ? $originalWidth : $originalHeight;
$font_size = $dimension ? $_font_size * ($dimension / 1000) * 1.2 : $_font_size;
if ($font_size > $_font_size)
{
$font_size = $_font_size;
}
if ($font_size < 14)
{
$font_size = 14;
}
$TextSize = @ImageTTFBBox($font_size, 0, $font, $text) or die;
$TextWidth = abs($TextSize[2]) + abs($TextSize[0]);
$TextHeight = abs($TextSize[7]) + abs($TextSize[1]);
$watermarkImage = imagecreatetruecolor($TextWidth, $TextHeight);
imagealphablending($watermarkImage, false);
imagesavealpha($watermarkImage, true);
$bgText = imagecolorallocatealpha($watermarkImage, 255, 255, 255, 127);
imagefill($watermarkImage, 0, 0, $bgText);
$wmTransp = 127 - ($opacity * 1.27);
$rgb = self::hex2rgb($color, false);
$colorResource = imagecolorallocatealpha($watermarkImage, $rgb[0], $rgb[1], $rgb[2], $wmTransp);
// Create watermark
imagettftext($watermarkImage, $font_size, 0, 0, abs($TextSize[5]), $colorResource, $font, $text);
return $watermarkImage;
}
/**
* Apply the watermark.
*
* @param array $opts
*
* @return void
*/
public static function applyWatermark($opts = [])
{
$defaults = [
'source' => null,
'destination' => null,
'preset' => 'custom',
'type' => 'text',
'text' => null,
'position' => 'bottom-right',
'angle' => 0,
'opacity' => 50,
'size' => 30,
'color' => '#fff'
];
$opts = array_merge($defaults, $opts);
if (!$opts['source'])
{
return false;
}
if (!is_file($opts['source']))
{
return false;
}
$destination = $opts['destination'] ? $opts['destination'] : $opts['source'];
$originalImage = new JoomlaImage($opts['source']);
// Get the dimensions of the original image
$originalWidth = $originalImage->getWidth();
$originalHeight = $originalImage->getHeight();
$watermarkSource = null;
switch ($opts['type'])
{
case 'image':
$watermarkSource = $opts['image'];
break;
case 'text':
default:
if (!$watermarkText = self::getWatermarkText($opts['text_preset'], $opts['text'], $destination))
{
return;
}
$watermarkSource = self::createWatermarkText($watermarkText, (int) $opts['size'], 100, $opts['color'], $originalWidth, $originalHeight);
break;
}
if (!$watermarkSource)
{
return;
}
$original_image_mime = $originalImage->getImageFileProperties($opts['source'])->mime;
// Create the final image
$finalImage = imagecreatetruecolor($originalWidth, $originalHeight);
$watermarkOpacity = (int) $opts['opacity'];
// Add a black background color if the image is PNG and watermark opacity is not 100, as imagecopymerge() doesn't work with transparent PNG images
if ($original_image_mime === 'image/png')
{
if ($watermarkOpacity === 100)
{
imagesavealpha($finalImage, true);
$trans_background = imagecolorallocatealpha($finalImage, 0, 0, 0, 127);
imagefill($finalImage, 0, 0, $trans_background);
}
else
{
$black = imagecolorallocate($finalImage, 0, 0, 0);
imagefill($finalImage, 0, 0, $black);
}
}
// Copy original image to the final image
imagecopy($finalImage, $originalImage->getHandle(), 0, 0, 0, 0, $originalWidth, $originalHeight);
// Get watermark image
$watermarkImage = new JoomlaImage($watermarkSource);
// Rotate it
if ($opts['angle'])
{
$angle = $opts['angle'] ? 360 - (int) $opts['angle'] : 0;
$watermarkImage->rotate($angle, -1, false);
}
// Get the dimensions of the watermark image
$watermarkWidth = $watermarkImage->getWidth();
$watermarkHeight = $watermarkImage->getHeight();
if (!$watermarkWidth || !$watermarkHeight)
{
return false;
}
// Final watermark width/height
$width = $watermarkWidth;
$height = $watermarkHeight;
// Scale watermark image
if ($opts['type'] === 'image')
{
$scaleFactor = min($originalWidth / $watermarkWidth, $originalHeight / $watermarkHeight);
// Calculate the new dimensions of the watermark image
$width = $watermarkWidth * $scaleFactor;
$height = $watermarkHeight * $scaleFactor;
if ($width > $watermarkWidth)
{
$width = $watermarkWidth;
$height = $watermarkHeight;
}
}
$width = (int) $width;
$height = (int) $height;
list($dest_x, $dest_y) = self::getWatermarkPosition($opts['position'], $originalWidth, $originalHeight, $width, $height);
// Resize watermark image before applying it into the final image
$watermarkImage = $watermarkImage->resize($width, $height);
// Copy the watermark image into the final image
if ($watermarkOpacity === 100)
{
imagecopy($finalImage, $watermarkImage->getHandle(), round($dest_x), round($dest_y), 0, 0, $width, $height);
}
else
{
self::imagecopymerge_alpha($finalImage, $watermarkImage->getHandle(), round($dest_x), round($dest_y), 0, 0, $width, $height, $watermarkOpacity);
}
// Save final image
switch ($original_image_mime)
{
case 'image/gif':
imagegif($finalImage, $destination);
break;
case 'image/webp':
imagewebp($finalImage, $destination, 70);
break;
case 'image/png':
imagepng($finalImage, $destination, 6);
break;
default:
imagejpeg($finalImage, $destination, 70);
break;
}
imagedestroy($finalImage);
$originalImage->destroy();
$watermarkImage->destroy();
}
/**
* imagecopy but with alpha channel support.
*
* @param object $dst_im
* @param object $src_im
* @param integer $dst_x
* @param integer $dst_y
* @param integer $src_x
* @param integer $src_y
* @param integer $src_w
*
* @return void
*/
public static function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct)
{
$cut = imagecreatetruecolor($src_w, $src_h);
// copying relevant section from background to the cut resource
imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
// copying relevant section from watermark to the cut resource
imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
// insert cut resource to destination image
imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct);
}
/**
* Returns the watermark position.
*
* @param string $position
* @param integer $originalWidth
* @param integer $originalHeight
* @param integer $width
* @param integer $height
*
* @return array
*/
public static function getWatermarkPosition($position, $originalWidth, $originalHeight, $width, $height)
{
/**
* Position watermark based on given position.
*
* top-left
* top-center
* top-right
* center-left
* center-center
* center-right
* bottom-left
* bottom-center
* bottom-right
*/
$position = explode('-', $position);
// Padding from corner
$yPOS = $xPOS = 10;
$dest_x = $dest_y = 0;
if (isset($position[0]))
{
switch ($position[0])
{
case 'top':
$dest_y = 0 + $yPOS;
break;
case 'center':
$dest_y = round($originalHeight / 2) - round($height / 2);
break;
case 'bottom':
$dest_y = $originalHeight - $height - $yPOS;
break;
}
}
if (isset($position[1]))
{
switch ($position[1])
{
case 'left':
$dest_x = 0 + $xPOS;
break;
case 'center':
$dest_x = round($originalWidth / 2) - round($width / 2);
break;
case 'right':
$dest_x = $originalWidth - $width - $xPOS;
break;
}
}
return [$dest_x, $dest_y];
}
/**
* Returns the watermark text.
*
* @param string $preset
* @param string $text
* @param string $filename
*
* @return string
*/
public static function getWatermarkText($preset = '', $text = '', $filename = '')
{
switch ($preset)
{
case 'site_name':
$text = Factory::getApplication()->get('sitename');
break;
case 'site_url':
$text = Uri::root();
break;
case 'custom':
$st = new \NRFramework\SmartTags();
// Add file Smart Tags
$file_data = File::pathinfo($filename);
$source_basename = $file_data['basename'];
$file_data['filename'] = $file_data['filename'];
$file_data['basename'] = $source_basename;
$st->add($file_data, 'file.');
$text = $st->replace($text);
break;
}
return $text;
}
/**
* Converts hexidecimal color value to rgb values and returns as array/string
*
* @param string $hex
* @param bool $asString
*
* @return array|string
*/
public static function hex2rgb($hex, $asString = false)
{
// strip off any leading #
if (0 === strpos($hex, '#'))
{
$hex = substr($hex, 1);
}
else if (0 === strpos($hex, '&H'))
{
$hex = substr($hex, 2);
}
// break into hex 3-tuple
$cutpoint = ceil(strlen($hex) / 2)-1;
$rgb = explode(':', wordwrap($hex, $cutpoint, ':', $cutpoint), 3);
// convert each tuple to decimal
$rgb[0] = (isset($rgb[0]) ? hexdec($rgb[0]) : 0);
$rgb[1] = (isset($rgb[1]) ? hexdec($rgb[1]) : 0);
$rgb[2] = (isset($rgb[2]) ? hexdec($rgb[2]) : 0);
return ($asString ? "{$rgb[0]} {$rgb[1]} {$rgb[2]}" : $rgb);
}
} Cache.php 0000644 00000005275 15235314576 0006304 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
/**
* This file is deprecated. Use CacheManager instead of Cache.
*/
namespace NRFramework;
defined('_JEXEC') or die;
use \NRFramework\CacheManager;
use \Joomla\CMS\Factory;
/**
* Caching mechanism
*/
class Cache
{
/**
* Check if has alrady exists in memory
*
* @param string $hash The hash string
*
* @return boolean
*/
static public function has($hash)
{
$cache = CacheManager::getInstance(Factory::getCache('tassos', ''));
return $cache->has($hash);
}
/**
* Returns hash value
*
* @param string $hash The hash string
* @param string $clone Why the hell we clone objects here?
*
* @return mixed False on error, Object on success
*/
static public function get($hash, $clone = true)
{
$cache = CacheManager::getInstance(Factory::getCache('tassos', ''));
return $cache->get($hash, $clone);
}
/**
* Sets on memory the hash value
*
* @param string $hash The hash string
* @param mixed $data Can be string or object
*
* @return mixed
*/
static public function set($hash, $data)
{
$cache = CacheManager::getInstance(Factory::getCache('tassos', ''));
return $cache->set($hash, $data);
}
/**
* Reads hash value from memory or file
*
* @param string $hash The hash string
* @param boolean $force If true, the filesystem will be used as well on the /cache/ folder
*
* @return mixed The hash object valuw
*/
static public function read($hash, $force = false)
{
$cache = CacheManager::getInstance(Factory::getCache('tassos', ''));
return $cache->read($hash, $force);
}
/**
* Writes hash value in cache folder
*
* @param string $hash The hash string
* @param mixed $data Can be string or object
* @param integer $ttl Expiration duration in milliseconds
*
* @return mixed The hash object value
*/
static public function write($hash, $data, $ttl = 0)
{
$cache = CacheManager::getInstance(Factory::getCache('tassos', ''));
return $cache->write($hash, $data, $ttl);
}
/**
* Memoize a function to run once per runtime
*
* @param string $key The key to store the result of the callback
* @param callback $callback The callable anonymous function to call
*
* @return mixed
*/
static public function memo($key, callable $callback)
{
$hash = md5($key);
if (Cache::has($hash))
{
return Cache::get($hash);
}
return Cache::set($hash, $callback());
}
} Extension.php 0000644 00000047624 15235314576 0007261 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Http\HttpFactory;
defined( '_JEXEC' ) or die( 'Restricted access' );
class Extension
{
/**
* Indicates the base url of Tassos.gr Joomla Extensions
*
* @var string
*/
public static $product_base_url = 'https://www.tassos.gr/joomla-extensions';
/**
* Array including already loaded extensions
*
* @var array
*/
public static $cache = [];
/**
* Get extension ID
*
* @param string $element The extension element name
* @param string $type The extension type: component, plugin, library e.t.c
* @param mixed $folder The plugin folder: system, content e.t.c
*
* @return mixed False on failure, Integer on success
*/
public static function getID($element, $type = 'component', $folder = null)
{
if (!$extension = self::get($element, $type, $folder))
{
return false;
}
return (int) $extension['extension_id'];
}
public static function getModuleByID($module_id = null)
{
if (!$module_id)
{
return;
}
$hash = 'get_module_by_id_' . $module_id;
// Render modal once
if ($module_data = Cache::get($hash))
{
return $module_data;
}
// Let's call the database
$db = Factory::getDBO();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__modules'))
->where($db->quoteName('id') . ' = ' . $module_id);
$db->setQuery($query);
$module_data = $db->loadAssoc();
Cache::set($hash, $module_data);
return $module_data;
}
/**
* Get extension data by ID
*
* @param string $extension_id The extension primary key
*
* @return void
*/
public static function getByID($extension_id)
{
// Check if element is already cached
if (isset(self::$cache[$extension_id]))
{
return self::$cache[$extension_id];
}
// Let's call the database
$db = Factory::getDBO();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $extension_id);
$db->setQuery($query);
return self::$cache[$extension_id] = $db->loadAssoc();
}
/**
* Get extension information from database
*
* @param string $element The extension element name
* @param string $type The extension type: component, plugin, library e.t.c
* @param mixed $folder The plugin folder: system, content e.t.c
*
* @return array
*/
public static function get($element, $type = 'component', $folder = null)
{
// Check if element is already cached
$hash = md5($element . '_' . $type . '_' . $folder);
if (isset(self::$cache[$hash]))
{
return self::$cache[$hash];
}
// Let's call the database
$db = Factory::getDBO();
switch ($type)
{
case 'component':
$element = 'com_' . str_replace('com_', '', $element);
break;
case 'module':
$element = 'mod_' . str_replace('mod_', '', $element);
break;
}
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote($type));
if (!is_null($folder))
{
$query->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
}
$db->setQuery($query);
return self::$cache[$hash] = $db->loadAssoc();
}
/**
* Get framework plugin data
*
* @return array
*/
public static function getFramework()
{
return self::get('nrframework', 'plugin', 'system');
}
/**
* Helper method to check if a plugin is enabled
*
* @param string $element The extension element name
* @param string $type The extension type: component, plugin, library e.t.c
*
* @return boolean
*/
public static function pluginIsEnabled($element, $folder = 'system')
{
return self::isEnabled($element, 'plugin', $folder);
}
/**
* Update an extension's params.
*
* @param string $name The extension name
* @param string $type The extension type
* @param string $element The extension element
* @param array $params The new params
*
* @return bool
*/
public static function updateExtensionParams($name = '', $type = '', $element = '', $params = [])
{
if (empty($name) || empty($type) || empty($element) || empty($params))
{
return false;
}
// Update params
$db = Factory::getDBO();
// Update params
$query = $db->getQuery(true)
->update('#__extensions')
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('name') . ' = ' . $db->quote($name))
->where($db->quoteName('type') . ' = ' . $db->quote($type))
->where($db->quoteName('element') . ' = ' . $db->quote($element));
$db->setQuery($query);
$db->execute();
return true;
}
/**
* Helper method to check if a component is enabled
*
* @param string $element The component element name
*
* @return boolean
*/
public static function componentIsEnabled($element)
{
return self::isEnabled($element);
}
/**
* Checks if an extension is enabled
*
* @param string $element The extension element name
* @param string $type The extension type: component, plugin, library e.t.c
* @param mixed $folder The plugin folder: system, content e.t.c
*
* @return boolean
*/
public static function isEnabled($element, $type = 'component', $folder = 'system')
{
switch ($type)
{
case 'component':
if (!$extension = self::get($element))
{
return false;
}
return (bool) $extension['enabled'];
break;
case 'plugin':
if (!$extension = self::get($element, $type = 'plugin', $folder))
{
return false;
}
return (bool) $extension['enabled'];
break;
}
}
/**
* Checks if an extension is installed
*
* @param string $extension The extension element name
* @param string $type The extension's type
* @param string $folder Plugin folder
*
* @return boolean Returns true if extension is installed
*/
public static function isInstalled($extension, $type = 'component', $folder = 'system')
{
$db = Factory::getDbo();
switch ($type)
{
case 'component':
$extension_data = self::get('com_' . str_replace('com_', '', $extension));
return isset($extension_data['extension_id']);
break;
case 'plugin':
return file_exists(JPATH_PLUGINS . '/' . $folder . '/' . $extension . '/' . $extension . '.php');
case 'module':
return (file_exists(JPATH_ADMINISTRATOR . '/modules/mod_' . $extension . '/' . $extension . '.php')
|| file_exists(JPATH_ADMINISTRATOR . '/modules/mod_' . $extension . '/mod_' . $extension . '.php')
|| file_exists(JPATH_SITE . '/modules/mod_' . $extension . '/' . $extension . '.php')
|| file_exists(JPATH_SITE . '/modules/mod_' . $extension . '/mod_' . $extension . '.php')
);
case 'library':
return is_dir(JPATH_LIBRARIES . '/' . $extension);
}
return false;
}
/**
* Discover extension's name based on the query string
*
* @param boolean $translate If set to yes, the name will be returned translated
*
* @return string
*/
public static function getExtensionNameByRequest($translate = false)
{
$input = Factory::getApplication()->input;
$option = $input->get('option');
$name = '';
switch ($option)
{
case 'com_modules':
$name = 'com_smilepack';
break;
case 'com_fields':
$name = 'plg_system_acf';
break;
case 'com_plugins':
$plugin = self::getByID($input->get('extension_id'));
if (is_array($plugin))
{
$name = $plugin['name'];
}
break;
default:
$name = $option;
break;
}
if ($translate)
{
$name = explode(' - ', Text::_($name));
return end($name);
}
return $name;
}
/**
* Returns Tassos.gr extension checkout URL
*
* @param string $name The extension's element name
* @param bool $append_utm Set whether to append the UTM parameters
*
* @return string
*/
public static function getTassosExtensionUpgradeURL($name = null, $append_utm = true)
{
$name = is_null($name) ? strtolower(self::getExtensionNameByRequest()) : $name;
$suffix = $append_utm ? '?utm_source=Joomla&utm_medium=upgradebutton&utm_campaign=freeversion' : '';
return self::$product_base_url . '/' . self::getProductAlias($name) . '/upgrade-to-pro' . $suffix;
}
public static function getProductAlias($extension)
{
$extension = is_null($extension) ? self::getExtensionNameByRequest() : $extension;
switch ($extension)
{
case 'com_gsd': case 'plg_system_gsd': return 'google-structured-data';
case 'com_rstbox': return 'engagebox';
case 'com_convertforms': return 'convert-forms';
case 'com_smilepack': return 'smile-pack';
case 'plg_system_tweetme': return 'tweetme';
case 'plg_system_acf': return 'advanced-custom-fields';
}
}
public static function getExtensionName($extension_element = null)
{
if (!$extension_element)
{
return;
}
// Load extension's language file
Functions::loadLanguage($extension_element);
// Remove plugin folder prefix from plugins
return str_replace('System -', '', Text::_($extension_element));
}
public static function getProductURL($extension)
{
return self::$product_base_url . '/' . self::getProductAlias($extension);
}
public static function getPath($element)
{
$parts = explode('_', $element);
switch ($parts[0])
{
case 'com':
return JPATH_ADMINISTRATOR . '/components/' . $element;
case 'plg':
return JPATH_SITE . '/plugins/' . $parts[1] . '/' . $parts[2];
}
}
public static function getVersion($extension, $include_type = false)
{
$xml = self::getXML($extension);
if (!$xml || !isset($xml->version))
{
return;
}
$version = (string) $xml->version;
// If enabled, it returns EngageBox Pro
if ($include_type)
{
$isPro = self::isPro($extension);
$version_type = $isPro ? 'Pro' : 'Free';
$version .= ' ' . $version_type;
}
return $version;
}
public static function elementToAlias($element)
{
$parts = explode('_', $element);
return end($parts);
}
public static function getXML($element)
{
if (!$path = self::getPath($element))
{
return;
}
$extension_alias = self::elementToAlias($element);
$xml = $path . '/' . $extension_alias . '.xml';
return simplexml_load_file($xml);
}
/**
* Returns a URL where we can check for extension updates.
*
* @param strong $extension
*
* @return mixed Null of fail, String on success
*/
public static function getUpdateServer($extension)
{
$xml = self::getXML($extension);
if (!$xml || !isset($xml->updateservers))
{
return;
}
$updateserver = trim($xml->updateservers->server);
// Remove unwanted string added by Free / Pro versions
$pp = strpos($updateserver, '@');
if ($pp !== false)
{
$updateserver = substr($updateserver, 0, $pp);
}
return $updateserver;
}
/**
* Get the latest extension version from the remote update server
*
* @param string $extension
*
* @return mixed Null on failure, String on success
*/
public static function getLatestVersion($extension)
{
// Get the extension's update server URL
if (!$updateserver = self::getUpdateServer($extension))
{
return;
}
// Call the Update Server and make sure the response is valid
$response = HttpFactory::getHttp()->get($updateserver);
if ($response->code != 200 || strpos($response->body, '<updates>') === false)
{
return;
}
$body = new \SimpleXMLElement($response->body);
$version = (string) $body->update[0]->version;
return $version;
}
/**
* Check if we have the Pro version of the extension
*
* @param string $element
*
* @return bool
*/
public static function isPro($element)
{
if (!$path = self::getPath($element))
{
return false;
}
$versionFile = $path . '/version.php';
// If version file does not exist we assume a PRO version
if (!file_exists($versionFile))
{
return true;
}
require $versionFile;
// If the NR_PRO variable is not set we're probably under development mode. Assume a Pro version.
if (!isset($NR_PRO))
{
return true;
}
return (bool) $NR_PRO;
}
/**
* Checks whether an extension is outdated.
*
* @param string $extension
* @param int $days_old
*
* @return bool
*/
public static function isOutdated($extension, $days_old = 120)
{
$versionFile = Functions::getExtensionPath($extension) . "/version.php";
if (!file_exists($versionFile))
{
return false;
}
require $versionFile;
if (!isset($RELEASE_DATE))
{
return false;
}
if (!$then = strtotime($RELEASE_DATE))
{
return false;
}
$days_old = (int) $days_old;
$now = time();
$diff = $now - $then;
$days_diff = round($diff / (60 * 60 * 24));
if ($days_diff <= $days_old)
{
return false;
}
return true;
}
/**
* Checks whether the geolocation plugin needs an update.
*
* @return bool
*/
public static function geoPluginNeedsUpdate()
{
// Check if TGeoIP plugin is enabled
if (!self::pluginIsEnabled('tgeoip'))
{
return false;
}
$plugin_path = JPATH_PLUGINS . '/system/tgeoip/';
// Load plugin language (Needed by Joomla 4)
Factory::getLanguage()->load('plg_system_tgeoip', $plugin_path);
// Load TGeoIP classes
@include_once $plugin_path . 'vendor/autoload.php';
@include_once $plugin_path . 'helper/tgeoip.php';
if (!class_exists('TGeoIP'))
{
return false;
}
// Check if database needs update.
$geo = new \TGeoIP();
if (!$geo->needsUpdate())
{
return false;
}
// Database is too old and needs an update! Let's inform user.
return true;
}
/**
* Returns the extension's JED URL.
*
* @param string $xml_folder
*
* @return string
*/
public static function getExtensionJEDURL($xml_folder = null)
{
if (empty($xml_folder))
{
return;
}
$url = 'https://extensions.joomla.org/extensions/extension/';
switch ($xml_folder) {
case 'com_smilepack':
$url .= 'smile-pack';
break;
case 'com_rstbox':
$url .= 'style-a-design/popups-a-iframes/engage-box';
break;
case 'com_convertforms':
$url .= 'contacts-and-feedback/forms/convert-forms';
break;
case 'plg_system_acf':
$url .= 'authoring-a-content/content-construction/advanced-custom-fields';
break;
case 'plg_system_gsd':
$url .= 'search-a-indexing/web-search/google-structured-data';
break;
case 'tm_mailchimpuserautoadd':
$url .= 'marketing/mailing-a-newsletter-bridges/user-auto-add-to-mailchimp-for-joomla';
break;
case 'tweetme':
$url .= 'social-web/social-share/tweetme';
break;
case 'mod_webhotelier':
$url .= 'vertical-markets/booking-a-reservations/webhotelier-booking-form';
break;
}
return $url;
}
/**
* Returns the installation date of an extension given its extension element.
*
* @param string $element
*
* @return string
*/
public static function getInstallationDate($element = null)
{
$alias = self::getExtensionDataFileAlias($element);
$path = self::getExtensionsDataFilePath();
// If file does not exist, abort
if (!file_exists($path))
{
return;
}
// If file exists, retrieve its contents
$content = file_get_contents($path);
// Decode it
if (!$content = json_decode($content, true))
{
return;
}
// If no installation date exists, abort
if (!isset($content[$alias]))
{
return;
}
// Ensure install date exists
if (!isset($content[$alias]['install_date']))
{
return;
}
return $content[$alias]['install_date'];
}
/**
* Sets the installation date of an extension.
*
* @param string $element
* @param string $install_date
*
* @return bool
*/
public static function setInstallationDate($element = null, $install_date = null)
{
$alias = self::getExtensionDataFileAlias($element);
$path = self::getExtensionsDataFilePath();
// If file does not exist, abort
if (!file_exists($path))
{
\NRFramework\File::createDirs(dirname($path));
file_put_contents($path, json_encode([
$alias => [
'install_date' => $install_date
]
]));
return;
}
// If file exists, retrieve its contents
$content = file_get_contents($path);
// Decode it
$content = json_decode($content, true);
if (!isset($content[$alias]))
{
$content[$alias]['install_date'] = $install_date;
}
else
{
foreach ($content as $key => &$value)
{
if ($key !== $alias)
{
continue;
}
if (isset($value['install_date']))
{
return false;
}
$value['install_date'] = $install_date;
}
}
file_put_contents($path, json_encode($content));
}
/**
* Returns all extensions file details.
*
* @return array
*/
public static function getExtensionsFileDetails()
{
$file = self::getExtensionsDataFilePath();
if (!file_exists($file))
{
return [];
}
if (!$data = file_get_contents($file))
{
return [];
}
if (!$data = json_decode($data, true))
{
return [];
}
return $data;
}
/**
* Returns an extension's alias used to find an extensions data within the extensions.json data file.
*
* @param string $element
*
* @return string
*/
public static function getExtensionDataFileAlias($element)
{
$element = str_replace('com_', '', $element);
switch ($element) {
case 'rstbox':
$element = 'engagebox';
break;
case 'smilepack':
$element = 'smile-pack';
break;
}
return $element;
}
/**
* The file path that stores all extensions data.
*
* @return string
*/
public static function getExtensionsDataFilePath()
{
return JPATH_SITE . '/media/plg_system_nrframework/data/extensions.json';
}
/**
* Returns all extensions details.
*
* @return array
*/
public static function getExtensionsDetails()
{
return [
'smilepack' => [
'extension' => 'com_smilepack',
'type' => 'component'
],
'engagebox' => [
'extension' => 'com_rstbox',
'type' => 'component'
],
'gsd' => [
'extension' => 'com_gsd',
'type' => 'component'
],
'acf' => [
'extension' => 'acf',
'type' => 'plugin'
],
'convertforms' => [
'extension' => 'com_convertforms',
'type' => 'component'
]
];
}
/**
* Returns the number of tassos.gr installed extensions.
*
* @return int
*/
public static function getTotalInstalledExtensions()
{
$installed = 0;
foreach (self::getExtensionsDetails() as $key => $value)
{
if (!self::isInstalled($value['extension'], $value['type']))
{
continue;
}
$installed++;
}
return $installed;
}
/**
* Returns the number of users active subscription plans.
*
* @param array $license_data
*
* @return int
*/
public static function getUserTotalPaidPlans($license_data = [])
{
if (!$license_data)
{
return 0;
}
$count = 0;
foreach ($license_data as $key => $value)
{
if (!isset($value['active']))
{
continue;
}
if (!$value['active'])
{
continue;
}
$count++;
}
return $count;
}
} WebClient.php 0000644 00000006333 15235314576 0007151 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework;
defined( '_JEXEC' ) or die( 'Restricted access' );
class WebClient
{
/**
* Joomla Application Client
*
* @var object
*/
public static $client;
/**
* Get visitor's Device Type
*
* @param string $ua User Agent string, if null use the implicit one from the server's enviroment
*
* @return string The client's device type. Can be: tablet, mobile, desktop
*/
public static function getDeviceType($ua = null)
{
$detect = new \NRFramework\Vendor\MobileDetect(null, $ua);
return ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'mobile') : 'desktop');
}
/**
* Get visitor's Operating System
*
* @param string $ua User Agent string, if null use the implicit one from the server's enviroment
*
* @return string Possible values: any of JApplicationWebClient's OS constants (except 'iphone' and 'ipad'),
* 'ios', 'chromeos'
*/
public static function getOS($ua = null)
{
// detect iOS and CromeOS (not handled by JApplicationWebClient)
$ua = self::getClient($ua)->userAgent;
$ios_regex = '/iPhone|iPad|iPod/i';
if (preg_match($ios_regex, $ua))
{
return 'ios';
}
$chromeos_regex = '/CrOS/i';
if (preg_match($chromeos_regex, $ua))
{
return 'chromeos';
}
// use JApplicationWebClient for OS detection
$platformInt = self::getClient($ua)->platform;
$constants = self::getClientConstants();
if (isset($constants[$platformInt]))
{
return strtolower($constants[$platformInt]);
}
}
/**
* Get visitor's Browser name / version
*
* @param string $ua User Agent string, if null use the implicit one from the server's enviroment
*
* @return array
*/
public static function getBrowser($ua = null)
{
$browser = new \Joomla\CMS\Environment\Browser($ua);
// Keep IE's name as 'ie' instead of 'msie' to prevent breaking existing assignments
$browserName = $browser->getBrowser() == 'msie' ? 'ie' : $browser->getBrowser();
return [
'name' => $browserName,
'version' => $browser->getVersion()
];
}
/**
* Get the constants from JApplicationWebClient as an array using the Reflection API
*
* @return array
*/
private static function getClientConstants()
{
$r = new \ReflectionClass('\\Joomla\\Application\\Web\\WebClient');
$constantsArray = $r->getConstants();
// flip the associative array
return array_flip($constantsArray);
}
/**
* Get the Application Client helper
* see https://api.joomla.org/cms-3/classes/Joomla.Application.Web.WebClient.html
*
* @param string $ua User Agent string, if null use the implicit one from the server's enviroment
*
* @return object
*/
public static function getClient($ua = null)
{
if (is_object(self::$client) && $ua == null)
{
return self::$client;
}
return (self::$client = new \Joomla\Application\Web\WebClient($ua));
}
} Parser/Lexer.php 0000644 00000010252 15235314576 0007603 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Tokens;
/**
* Lexer base class
*
* TODO: Rename to Tokenizer??
*/
abstract class Lexer
{
/**
* EOF character
*/
const EOF = -1;
/**
* Tokens instance
*
* @var NRFramework\Parser\Tokens
*/
protected $tokens = null; // Tokens instance
/**
* Input string
*
* @var string
*/
protected $input;
/**
* Input string length
*/
protected $length;
/**
* The index of the current character
* in the input string
*
* @var integer
*/
protected $index = 0;
/**
* Current character in input string
*
* @var string
*/
protected $cur;
/**
* A Mark(position) inside the input string.
* Used when matching ahead of the 'current' character
*
* @var integer
*/
protected $mark = 0;
/**
* Holds the Lexer's state
*
* @var object
*/
protected $state;
/**
* Lexer constructor
*
* @param string $input
*/
public function __construct($input)
{
$this->input = $input;
$this->length = strlen($input);
$this->cur = $this->length >= 1 ? $this->input[0] : Lexer::EOF;
$this->tokens = new Tokens();
// inititalize state
$this->state = new \StdClass();
$this->state->skip_whitespace = true;
$this->state->tokenize_content = false;
}
/**
* Returns the next token from the input string.
*
* @return NRFramework\Parser\Token
*/
abstract function nextToken();
/**
* Moves n characters ahead in the input string.
* Returns all n characters.
* Detects "end of file".
*
* @param integer $n Number of characters to advance
* @return string The n previous characters
*/
public function consume($n = 1)
{
$prev = '';
for ($i=0; $i < $n; $i++)
{
$prev .= $this->cur;
if ( ($this->index + 1) >= $this->length)
{
$this->cur = Lexer::EOF;
break;
}
else
{
$this->index++;
$this->cur = $this->input[$this->index];
}
}
return $prev;
}
/**
* Sets the skip_whitespce state
*
* @param boolean $skip
* @return void
*/
public function setSkipWhitespaceState($skip = true)
{
$this->state->skip_whitespace = $skip;
}
/**
* Sets the tokenize_content state
*
* @param bool
* @return void
*/
public function setTokenizeContentState($state = true)
{
$this->state->tokenize_content = $state;
}
/**
* Gets the tokenize_content state
*
* @param bool
* @return bool
*/
public function getTokenizeContentState()
{
return $this->state->tokenize_content;
}
/**
* Marks the current index
*
* @return void
*/
public function mark()
{
$this->mark = $this->index;
}
/**
* Reset index to previously marked position (or at the start of the stream if not marked)
*
* @return void
*/
public function reset()
{
$this->index = $this->mark;
$this->cur = $this->input[$this->index];
$this->mark = 0;
}
/**
* Get the token types array from the Tokens instance
*
* @return void
*/
public function getTokensTypes()
{
return $this->tokens->getTypes();
}
/**
* Returns the current position in the input stream
*
* @return integer
*/
public function getStreamPosition()
{
return $this->index;
}
/**
* whitespace : (' '|'\t'|'\n'|'\r')
* Ignores any whitespace while advancing
* @return null
*/
protected function whitespace()
{
while (preg_match('/\s+/', $this->cur)) $this->consume();
}
}
Parser/ShortcodeLexer.php 0000644 00000022104 15235314576 0011455 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
/**
* ShortcodeLexer
*
* Tokenizes a string using the following grammar.
* Acts as the input "stream" for NRFramework\Parser\ShortcodeParser
*
* Tokens:
* -------
* sc_open : shortcode tag opening character(s), default: {
* sc_close : shortcode tag closing character(s), default: }
* if_keyword : if keyword, default: 'if'
* endif_keyword : endif keyword, default: '/if'
* text : any character sequence
* text_preserved : any character sequence with quoted values preserved
* whitespace : ' ' | '\r' | '\n' | '\t'
*/
class ShortcodeLexer extends Lexer
{
/**
* Shortcode opening character(s) (default: {)
*
* @var string
*/
protected $sc_open_char;
/**
* Shortcode closing character(s) (default: })
*
* @var string
*/
protected $sc_close_char;
/**
* if keyword (default: 'if')
*
* @var string
*/
protected $if_keyword;
/**
* endif keyword (default: '/if')
*
* @var string
*/
protected $endif_keyword;
/**
* ShortcodeLexer constructor
*
* @param string $input
* @param object $options
*/
public function __construct($input, $options = null)
{
parent::__construct($input);
$this->tokens->addType('sc_open');
$this->tokens->addType('sc_close');
$this->tokens->addType('if_keyword');
$this->tokens->addType('endif_keyword');
$this->tokens->addType('char');
$this->sc_open_char = $options->tag_open_char ?? '{';
$this->sc_close_char = $options->tag_close_char ?? '}';
$this->if_keyword = $options->if_keyword ?? 'if';
$this->endif_keyword = '/' . $this->if_keyword;
}
/**
* Returns the next token from the input string
*
* @return RestrictContent\Parser\Token
*/
public function nextToken()
{
static $if_flag = false;
while ($this->cur !== Lexer::EOF)
{
if ($this->state->skip_whitespace && preg_match('/\s+/', $this->cur))
{
$this->whitespace();
continue;
}
if ($this->predictScOpen())
{
$this->setTokenizeContentState(false);
$this->setSkipWhitespaceState(true);
return $this->sc_open();
}
else if ($this->predictScClose())
{
$this->setTokenizeContentState(true);
$this->setSkipWhitespaceState(false);
return $this->sc_close();
}
// check for if/endif
else if ($this->predictIf(false))
{
return $this->_if();
}
else if ($this->predictEndif(false))
{
return $this->_endif();
}
// check for text
else {
$preserve_quoted_values = !$this->getTokenizeContentState();
$token = $this->text($preserve_quoted_values);
return $token;
}
}
// return EOF token at the end of stream
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* Predicts an upcoming 'if' keyword from the input stream
*
* @param bool $reset Reset to the marked position when the keyword is found
* @return bool
*/
protected function predictIf($reset = true)
{
$this->mark();
$tmp = $this->consume(2);
if ($tmp === $this->if_keyword)
{
if ($reset)
{
$this->reset();
}
return true;
}
$this->reset();
return false;
}
/**
* Predicts an upcoming 'endif' keyword from the input stream
*
* @param bool $reset Reset to the marked position when the keyword is found
* @return bool
*/
protected function predictEndif($reset = true)
{
$this->mark();
$tmp = $this->consume(3);
if ($tmp === $this->endif_keyword)
{
if ($reset)
{
$this->reset();
}
return true;
}
$this->reset();
return false;
}
/**
* Predicts any upcoming keyword
*
* @return bool
*/
protected function predictKeywords()
{
return $this->predictIf() || $this->predictEndif();
}
/**
* Predicts any upcoming special character
*
* @return bool
*/
protected function predictSpecialChars()
{
return $this->predictScOpen() || $this->predictScClose();
}
/**
* Predicts upcoming shortcode opening character(s), default: {
*
* @return bool
*/
protected function predictScOpen()
{
$sc_length = \strlen($this->sc_open_char);
$res = false;
$this->mark();
$tmp = $this->consume($sc_length);
if ($tmp === $this->sc_open_char)
{
$res = true;
}
$this->reset();
return $res;
}
/**
* Predicts upcoming shortcode closing character(s), default: {
*
* @return bool
*/
protected function predictScClose()
{
$sc_length = \strlen($this->sc_close_char);
$res = false;
$this->mark();
$tmp = $this->consume($sc_length);
if ($tmp === $this->sc_close_char)
{
$res = true;
}
$this->reset();
return $res;
}
/**
* sc_open : shortcode tag opening character, default: {
*
* @return Token
*/
protected function sc_open()
{
$pos = $this->index;
$length = \strlen($this->sc_open_char);
$this->consume($length);
return $this->tokens->create('sc_open', $this->sc_open_char, $pos);
}
/**
* sc_close : shortcode tag closeing character, default: }
*
* @return Token
*/
protected function sc_close()
{
$pos = $this->index;
$length = \strlen($this->sc_close_char);
$this->consume($length);
return $this->tokens->create('sc_close', $this->sc_close_char, $pos);
}
/**
* if_keyword, default: 'if'
*
* @return Token
*/
protected function _if()
{
return $this->tokens->create('if_keyword', $this->if_keyword, $this->index - \strlen($this->if_keyword));
}
/**
* endif_keyword, default: '/if'
*
* @return Token
*/
protected function _endif()
{
return $this->tokens->create('endif_keyword', $this->endif_keyword, $this->index - \strlen($this->endif_keyword));
}
/**
* text : any character sequence
*
* @param bool $preserve Preserve keywords and special characters inside quotes
* @return Token
*/
protected function text($preserve)
{
if ($preserve)
{
return $this->text_preserved();
}
$pos = $this->index;
$buf = '';
while ($this->cur !== Lexer::EOF)
{
if ($this->predictKeywords() || $this->predictSpecialChars())
{
return $this->tokens->create('text', $buf, $pos);
}
$buf .= $this->cur;
$this->consume();
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* text_preserved : any character sequence with quoted values preserved
*
* @return Token
*/
protected function text_preserved()
{
$quote_queue = [];
$buf = '';
$pos = $this->index;
while ($this->cur !== Lexer::EOF)
{
// manage quote parsing
if ($this->cur == '"' || $this->cur == "'")
{
if ($this->cur == end($quote_queue))
{
// remove last added quote
array_pop($quote_queue);
}
else
{
// add quote to the queue
array_push($quote_queue, $this->cur);
}
}
// End parsing when any keyword or special character is found
// handles quoted values
if ($this->predictKeywords() || $this->predictSpecialChars())
{
// return the expression's text if no quotes are open
if (empty($quote_queue))
{
return $this->tokens->create('text_preserved', trim($buf), $pos);
}
}
// add current character to buffer
$buf .= $this->cur;
$this->consume();
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
}
Parser/ConditionParser.php 0000644 00000024305 15235314576 0011633 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Parser;
use NRFramework\Parser\ConditionLexer;
/**
* ConditionParser
* LL(1) recursive-decent parser
* Uses NRFramework\Parser\ConditionLexer as input source
*
* Grammar:
* --------
* expr : condition (logic_op condition)* (option)*
* condition : {negate_op} alias (parameter)* | (alias|l_func) ({negate_op}? operator (values)? (parameter)*
* alias : {ident}
* values : value ({comma} value)*
* value : {quotedval} | ({literal} | {ident})+
* func : {ident} {l_paren} values {r_paren}
* l_func : func
* r_func : func
* parameter : {param} ({equals} value)?
* option : {ident} ({equals} value)?
* logic_op : {and} | {or}
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
*/
class ConditionParser extends Parser
{
/**
* Constructor
*
* @param ConditionLexer $input
*/
public function __construct(ConditionLexer $input)
{
parent::__construct($input, 2);
}
/**
* value : {quotedval} | ({literal} | {ident})+
*
* @return string
* @throws Exception
*/
public function value()
{
if ($this->lookahead[0]->type === 'quotedvalue')
{
$text = $this->lookahead[0]->text;
$this->match('quotedvalue');
return $text;
}
else if ($this->lookahead[0]->type !== 'ident' && $this->lookahead[0]->type !== 'literal')
{
throw new \Exception("Syntax error in ConditionParser::value(); expecting 'ident' or 'literal'; found {$this->lookahead[0]}");
}
$text = $this->lookahead[0]->text;
$this->consume();
while ($this->lookahead[0]->type === 'ident' || $this->lookahead[0]->type === 'literal')
{
$text .= ' ' . $this->lookahead[0]->text;
$this->consume();
}
return $text;
}
/**
* values : value ({comma} value)*
*
* @return array
*/
public function values()
{
$vals = [];
$vals[] = $this->value();
while ($this->lookahead[0]->type === 'comma')
{
$this->consume();
$vals[] = $this->value();
}
return $vals;
}
/**
* func : {ident} {l_paren} values {r_paren}
*
*/
public function func()
{
$func_name = $this->lookahead[0]->text;
$this->match('ident');
$this->match('l_paren');
if ($this->lookahead[0]->type === 'quotedvalue' ||
$this->lookahead[0]->type === 'ident' ||
$this->lookahead[0]->type === 'literal')
{
$func_args = $this->values();
}
$this->match('r_paren');
return ['func_name' => $func_name, 'func_args' => $func_args ?? []];
}
/**
* parameter : {param} ({equals} value)?
*
* @return string
*/
public function param()
{
$param = $this->lookahead[0]->text;
$value = true;
$this->match('param');
// If this is the 'context' parameter make sure that it appears as the last token
// if ($param === 'context')
// {
// $this->consume(); // consume the 'equals' operator
// $value = $this->value(); // expect a value
// if ($this->lookahead[0]->type !== 'EOF')
// {
// throw new \Exception("Syntax error in ConditionParser::param(); the 'context' parameter can only appear as the last token");
// }
// }
// else
if ($this->isOperator($this->lookahead[0]->type))
{
if ($this->lookahead[0]->type === 'equals')
{
$this->consume(); // consume the 'equals' operator
$value = $this->value(); // expect a value
}
else
{
// only the 'equals' operator is supported for the 'param' rule.
throw new \Exception("Syntax error in ConditionParser::param(); expecting 'equals', found {$this->lookahead[0]}");
}
}
return ['param' => $param, 'value' => $value];
}
/**
* alias : {ident}
*
* @return string
*/
public function alias()
{
$sel = $this->lookahead[0]->text;
$this->match('ident');
return $sel;
}
/**
* condition : {negate_op} alias (parameter)* | alias ({negate_op}? operator values)? (parameter)*
*
* @return object
*/
public function condition()
{
$result = [];
$operator = '';
$params = [];
$negate_op = false;
if ($this->lookahead[0]->type === 'negate_op')
{
$this->match('negate_op');
$operator = 'empty';
$result['alias'] = $this->alias();
}
else
{
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
{
$l_func = $this->func();
$result['l_func_name'] = $l_func['func_name'];
$result['l_func_args'] = $l_func['func_args'];
}
else
{
$result['alias'] = $this->alias();
}
if ($this->lookahead[0]->type === 'negate_op')
{
$this->match('negate_op');
$negate_op = true;
// expect an operator after '!'
if (!$this->isOperator($this->lookahead[0]->type))
{
throw new Exceptions\SyntaxErrorException("Expecting an 'operator' after '!', found {$this->lookahead[0]}");
}
}
if ($this->isOperator($this->lookahead[0]->type))
{
$operator = $this->operator();
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
{
$r_func = $this->func();
$result['r_func_name'] = $r_func['func_name'];
$result['r_func_args'] = $r_func['func_args'];
}
else if (
$this->lookahead[0]->type === 'quotedvalue' ||
$this->lookahead[0]->type === 'ident' ||
$this->lookahead[0]->type === 'literal'
)
{
$values = $this->values();
if (count($values) === 1)
{
$values = $values[0];
}
$result['values'] = $values;
}
}
}
while ($this->lookahead[0]->type === 'param')
{
$params[] = $this->param();
}
if (!$operator) {
$operator = 'empty';
$negate_op = true;
}
//
$_params = [];
foreach($params as $p)
{
$_params[$p['param']] = $p['value'];
}
$result['operator'] = $operator;
$result['negate_op'] = $negate_op;
$result['params'] = $_params;
return $result;
}
/**
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
*
* @return string
* @throws Exception
*/
public function operator()
{
if (!$this->isOperator($this->lookahead[0]->type))
{
throw new Exceptions\SyntaxErrorException("Expecting an 'operator', found " . $this->lookahead[0]);
}
$op = $this->lookahead[0]->type;
$this->consume();
return $op;
}
/**
* expr : condition ({logic_op} condition)* (option)*
*
* @return array The condition expression results
*/
public function expr()
{
$logic_op = 'and';
$res = [
'conditions' => [$this->condition()],
'logic_op' => 'and',
'context' => null,
'global_params' => []
];
if ($this->lookahead[0]->type === 'or')
{
$logic_op = 'or';
}
while ($this->lookahead[0]->type !== 'EOF')
{
$this->match($logic_op);
$res['conditions'][] = $this->condition();
}
$res['logic_op'] = $logic_op;
// check the last parsed condition for global parameters
$globalParams = [
'debug',
'dateformat',
'context',
'nopreparecontent',
'excludebots'
];
$last_params = $res['conditions'][count($res['conditions'])-1]['params'];
foreach(array_keys($last_params) as $param_key)
{
if (in_array(strtolower($param_key), $globalParams))
{
$res['global_params'][strtolower($param_key)] = $last_params[$param_key];
unset($res['conditions'][count($res['conditions'])-1]['params'][$param_key]);
}
}
// foreach ($last_params as $idx => $param)
// {
// if (in_array($param['param'], $globalParams))
// {
// $res['global_params'][$param['param']] = $param['value'];
// unset($res['conditions'][count($res['conditions'])-1]['params'][$idx]);
// }
// }
return $res;
}
/**
* Helper method that checks if the given Token is an operator.
*/
protected function isOperator($token_type)
{
return in_array($token_type, [
'equals',
'starts_with',
'ends_with',
'contains',
'contains_any',
'contains_all',
'contains_only',
'lt',
'lte',
'gt',
'gte',
'empty'
]);
}
}
Parser/Tokens.php 0000644 00000003557 15235314576 0010001 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* Tokens
* Holds token types and manages creation of new tokens
*/
class Tokens
{
/**
* Token types array
*
* @var array
*/
protected $types = [];
public function __construct()
{
// default types
$this->addType('invalid_token');
$this->addType('EOF');
}
/**
* Adds a new token type
*
* @param string $type
* @return $this
*/
public function addType($type)
{
if (!$this->hasType($type))
{
$this->types[] = $type;
}
return $this;
}
/**
* Gets a token type id (i.e. it's array index)
*
* @param string $type
* @return int|null
*/
public function getTypeId($type)
{
$id = array_search($type, $this->types);
$id = $id !== false ? $id : null;
return $id;
}
/**
* Returns the token types array
*
* @return array
*/
public function getTypes()
{
return clone $this->types;
}
/**
* Creates a new token
*
* @param string $type
* @param string $text
* @param integer $position, Position of token in the input stream
* @return Token
*/
public function create($type, $text, $position)
{
return new Token($type, $text, $position);
}
/**
* Checks if a type is registered
*
* @param string $type
* @return boolean
*/
public function hasType($type)
{
return (bool)array_search($type, $this->types);
}
}
Parser/ConditionsEvaluator.php 0000644 00000056575 15235314576 0012542 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use DateTime;
use DateTimeZone;
use Exception;
use Joomla\CMS\Factory;
class ConditionsEvaluator
{
/**
* Payload associative array
*
* @var array
*/
protected $payload;
/**
* Parsed conditions
*
* @var array
*/
protected $conditions;
/**
* Framework Condition aliases
*
* @var array
*/
protected $condition_aliases;
/**
* Debug flag
*
* @var bool
*/
protected $debug;
/**
* @param array $conditions The parsed conditions
* @param array $payload Shortcode parser payload
*/
public function __construct($conditions, $payload = null, $debug = false)
{
$this->conditions = $conditions;
$this->payload = $payload;
$this->debug = $debug;
$this->generateConditionAliasesMap();
}
/**
* @return array
*/
public function evaluate() : array
{
$results = [];
$caseSensitive = false;
foreach($this->conditions as $condition)
{
// case sensitivity param
if (array_key_exists('caseSensitive', $condition['params']))
{
$caseSensitive = strtolower($condition['params']['caseSensitive']) != 'false';
}
$result = [
'operator' => $condition['operator'],
'params' => $condition['params']
];
$l_value = null;
$r_value = null;
if(array_key_exists('r_func_name', $condition))
{
$r_value = $this->applyFunction($condition['r_func_name'], $condition['r_func_args']);
$result['r_func_name'] = $condition['r_func_name'];
$result['r_func_args'] = $condition['r_func_args'];
$result['r_func_val'] = $r_value;
}
else
{
$r_value = $condition['values'] ?? null;
}
if (array_key_exists('alias', $condition) && $this->isPayloadCondition($condition['alias']))
{
$l_value = $this->payload[$condition['alias']];
$result = array_merge($result, $this->evaluatePayloadCondition($l_value, $r_value, $condition['operator'], $caseSensitive));
$result['pass'] = $condition['negate_op'] ? !$result['pass'] : $result['pass'];
$result['actual_value'] = $this->payload[$condition['alias']];
}
else if (array_key_exists('alias', $condition) && $this->isFrameworkCondition($condition['alias']))
{
$result = array_merge($result, $this->evaluateFrameworkCondition($condition, $r_value));
}
else if (array_key_exists('l_func_name', $condition))
{
$l_value = $this->applyFunction($condition['l_func_name'], $condition['l_func_args']);
$result['l_func_name'] = $condition['l_func_name'];
$result['l_func_args'] = $condition['l_func_args'];
$result['l_func_val'] = $l_value;
$result = array_merge($result, $this->evaluatePayloadCondition($l_value, $r_value, $condition['operator'], $caseSensitive));
$result['pass'] = $condition['negate_op'] ? !$result['pass'] : $result['pass'];
}
// not a payload or framework condition with the 'empty' op
else if ($condition['operator'] === 'empty')
{
$result['pass'] = !$condition['negate_op'];
}
//
else
{
// Unknown condition
throw new Exceptions\InvalidConditionException($condition['alias']);
}
$results[] = $result;
}
return $results;
}
/**
*
*/
public function applyFunction($func_name, $args)
{
$arg_values = [];
foreach($args as $arg)
{
if ($this->isPayloadCondition($arg))
{
$arg_values[] = $this->payload[$arg];
}
else if ($this->isFrameworkCondition($arg))
{
$conditions_helper = \NRFramework\Conditions\ConditionsHelper::getInstance();
$framework_condition = $conditions_helper->getCondition($this->condition_aliases[strtolower($arg)]);
// Some framework condition don't implement the 'value()' method.
if (method_exists($framework_condition, 'value'))
{
$arg_values[] = $framework_condition->value();
}
else
{
throw new Exceptions\ConditionValueException($arg);
}
}
else
{
$arg_values[] = $arg;
}
}
switch(strtolower($func_name))
{
case 'count':
return $this->funcCount($arg_values);
case 'today':
return $this->funcToday();
case 'now':
return $this->funcNow();
case 'date':
return $this->funcDate($arg_values);
case 'datediff':
return $this->funcDateDiff($arg_values);
default:
throw new Exceptions\UnknownFunctionException($func_name);
}
}
/**
*
*/
public function funcCount($args)
{
if (count($args) !== 1)
{
throw new Exception("count() accepts 1 argument. " . count($args) . " were given.");
}
if (is_array($args[0]))
{
return count($args[0]);
}
else if (is_string($args[0]))
{
return mb_strlen($args[0]);
}
else
{
throw new Exception("count() accepts only strings and arrays.");
}
}
/**
*
*/
public function funcToday()
{
return (new DateTime('today'))->format('Y-m-d');
}
/**
*
*/
public function funcNow()
{
return new DateTime('now');
}
/**
*
*/
public function funcDate($args)
{
if (count($args) < 1 || count($args) > 3)
{
throw new Exception("date() accepts between 1 and 3 arguments. " . count($args) . " were given.");
}
if ($args[0] instanceof \DateTime || $args[0] instanceof \DateTimeImmutable)
{
return $args[0];
}
$date = $args[0];
$format = null;
if (count($args) > 1)
{
$format = $args[1] === 'null' ? null : $args[1];
}
$timezone = new \DateTimeZone($args[2] ?? Factory::getApplication()->get('offset','UTC'));
if ($format)
{
return \DateTime::createFromFormat('!'.$format, $date, $timezone);
}
return new \DateTime($date, $timezone);
}
/**
*
*/
public function funcDateDiff($args)
{
if (count($args) != 2)
{
throw new Exception("dateDiff() accepts 2 arguments. " . count($args) . " were given.");
}
$date1 = $this->convertToDateTime($args[0]);
$date2 = $this->convertToDateTime($args[1]);
return abs($date1->diff($date2)->days);
}
/**
* @var array $condition
*
* @return array Evaluation result
*/
protected function evaluatePayloadCondition($l_value, $r_value, $operator, $caseSensitive = false) : array
{
if (!$caseSensitive)
{
$l_value = $this->_lowercaseValues($l_value);
$r_value = $this->_lowercaseValues($r_value);
}
$result = [];
switch($operator)
{
case 'equals':
$result = $this->evaluateEquals($l_value, $r_value);
break;
case 'starts_with':
$result = $this->evaluateStartsWith($l_value, $r_value);
break;
case 'ends_with':
$result = $this->evaluateEndsWith($l_value, $r_value);
break;
case 'contains':
$result = $this->evaluateContains($l_value, $r_value);
break;
case 'contains_any':
$result = $this->evaluateContainsAny($l_value, $r_value);
break;
case 'contains_all':
$result = $this->evaluateContainsAll($l_value, $r_value);
break;
case 'contains_only':
$result = $this->evaluateContainsOnly($l_value, $r_value);
break;
case 'lt':
$result = $this->evaluateLessThan($l_value, $r_value);
break;
case 'lte':
$result = $this->evaluateLessThanEquals($l_value, $r_value);
break;
case 'gt':
$result = $this->evaluateGreaterThan($l_value, $r_value);
break;
case 'gte':
$result = $this->evaluateGreaterThanEquals($l_value, $r_value);
break;
case 'empty':
$result = $this->evaluateEmpty($l_value);
break;
default:
throw new Exceptions\UnknownOperatorException($operator);
}
return $result;
}
/**
* @var array $condition
*
* @return array Evaluation result
*/
public function evaluateFrameworkCondition($condition, $r_value)
{
$operator = $condition['operator'];
// Certain framework operators only work on single values.
// Force fail if the parsed condition contains more than one value.
if (in_array($operator, [
'contains',
'lt', 'lte',
'gt', 'gte',
'starts_with',
'ends_with'
]))
{
if (is_array($r_value) && !empty($r_value))
{
throw new Exceptions\UnsupportedValueOperandException($operator, false);
}
}
//
$conditions_helper = \NRFramework\Conditions\ConditionsHelper::getInstance();
$result = ['actual_value' => null];
// Transform 'caseSensitive' parameter to 'ignoreCase'
if (array_key_exists('caseSensitive', $condition['params']))
{
$condition['params']['ignoreCase'] = !$condition['params']['caseSensitive'];
}
// Instantiate the framework condition
$framework_condition = $conditions_helper->getCondition(
$this->condition_aliases[strtolower($condition['alias'])],
$r_value,
$operator,
$condition['params']
);
// Try to grab the actual condition's value if 'debug' is enabled.
if ($this->debug)
{
// Some framework conditions don't implement the 'value()' method.
if (method_exists($framework_condition, 'value'))
{
$result['actual_value'] = $framework_condition->value();
}
}
// Special handling for Date/Time framework conditions
if (in_array(strtolower($condition['alias']), ['date', 'time', 'datetime']))
{
$pass = $this->evaluatePayloadCondition($framework_condition->value(), $r_value, $operator)['pass'];
}
// Check if the condition passes using the 'passOne()' helper method
else
{
$pass = $conditions_helper->passOne(
$this->condition_aliases[strtolower($condition['alias'])],
$r_value,
$operator,
$condition['params']
);
}
$result['pass'] = $condition['negate_op'] ? !$pass : $pass;
return $result;
}
/**
* Generates an array mapping Condition aliases to Condition class names
*/
protected function generateConditionAliasesMap()
{
$conditions_namespace = 'NRFramework\\Conditions\\Conditions\\';
$dir_iterator = new \RecursiveDirectoryIterator(JPATH_PLUGINS . "/system/nrframework/NRFramework/Conditions/Conditions/");
$iterator = new \RecursiveIteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file)
{
$condition_class = str_replace(JPATH_PLUGINS . "/system/nrframework/NRFramework/Conditions/Conditions/", '', $file);
$condition_class = str_replace('.php', '', $condition_class);
$condition_class = str_replace('/', '\\', $condition_class);
if (class_exists($conditions_namespace . $condition_class))
{
$this->condition_aliases[strtolower($file->getBasename('.php'))] = $condition_class;
if (property_exists($conditions_namespace . $condition_class, 'shortcode_aliases'))
{
foreach(($conditions_namespace . $condition_class)::$shortcode_aliases as $alias)
{
$this->condition_aliases[$alias] = $condition_class;
}
}
}
}
}
/**
*
*/
protected function convertToDateTime($date, $format = null, $tz = null)
{
if ($tz == null)
{
$tz = Factory::getApplication()->getCfg('offset','UTC');
}
if ($date instanceof \DateTime || $date instanceof \DateTimeImmutable)
{
return $date;
}
try
{
if ($format)
{
return DateTime::createFromFormat($format, $date, $tz);
}
return new DateTime($date, new DateTimeZone($tz));
}
catch (\Throwable $t)
{
return null;
}
}
/**
*
*/
protected function isPayloadCondition($alias)
{
return $this->payload && array_key_exists($alias, $this->payload);
}
/**
*
*/
protected function isFrameworkCondition($alias)
{
return array_key_exists(strtolower($alias), $this->condition_aliases);
}
/**
* @return array Evaluation result
*/
protected function evaluateEquals($l_value, $r_value) : array
{
// are we comparing arrays?
if (is_array($l_value))
{
return $this->evaluateContainsAny($l_value, $r_value);
}
if (is_numeric($r_value))
{
return ['pass' => $l_value == $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date == $r_date
];
}
// generic equality test
return ['pass' => $l_value == $r_value];
}
/**
* @return array Evaluation result
*/
protected function evaluateStartsWith($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('startsWith', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('startsWith', false);
}
return ['pass' => $this->_starts_with($l_value, $r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateEndsWith($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('endsWith', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('endsWith', false);
}
return ['pass' => $this->_ends_with($l_value, $r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateContains($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('contains', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('contains', false);
}
return ['pass' => strlen($l_value) > 0 && strpos($l_value, $r_value) !== false];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsAny($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsAny', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => !empty(array_intersect($l_value, $r_value))];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsAll($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsAll', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => count(array_intersect($l_value, $r_value)) == count($r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsOnly($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsOnly', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => count(array_diff($l_value, $r_value)) == 0];
}
/**
* @return array Evaluation result
*/
protected function evaluateLessThan($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value < $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date < $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'lessThan' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateLessThanEquals($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value <= $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date <= $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'lessThanEquals' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateGreaterThan($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value > $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date > $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'greaterThan' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateGreaterThanEquals($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value >= $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date >= $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'greaterThanEquals' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateEmpty($payload_value) : array
{
// $payload_value = $this->payload[$payload_key];
if (is_array($payload_value))
{
return ['pass' => empty($payload_value)];
}
else if(is_string($payload_value))
{
$payload_value = trim($payload_value);
return ['pass' => empty($payload_value) || $payload_value == 'false'];
}
else if(is_bool($payload_value))
{
return ['pass' => !$payload_value];
}
return ['pass' => is_null($payload_value)];
}
/**
* @return bool
*/
protected function _starts_with($haystack, $needle)
{
return strlen($needle) > 0 && strncmp($haystack, $needle, strlen($needle)) === 0;
}
/**
* @return bool
*/
protected function _ends_with($haystack, $needle)
{
return strlen($needle) > 0 && substr($haystack, -strlen($needle)) === (string)$needle;
}
/**
* @return bool
*/
protected function _contains($haystack, $needle)
{
return strlen($needle) > 0 && strpos($haystack, $needle) !== false;
}
/**
* @return string|array
*/
protected function _lowercaseValues($value)
{
if (is_array($value))
{
foreach($value as $idx => $val)
{
if (is_string($val))
{
$value[$idx] = strtolower($val);
}
}
}
else if(is_string($value))
{
$value = strtolower($value);
}
return $value;
}
}
Parser/RingBuffer.php 0000644 00000014054 15235314576 0010561 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* RingBuffer
*
* A circular buffer of fixed length.
* This class essentially implements a fixed-size FIFO stack but with
* "convenient" accessor methods compared to manually handling a vanilla PHP array.
*
* Used by NRFramework\Parser\Parser and NRFramework\Parser\Lexer.
*/
class RingBuffer implements \Countable, \ArrayAccess, \Iterator
{
/**
* Iterator position
* @var int
*/
protected $iterator_position = 0;
/**
* Position of the next element
* @var integer
*/
protected $position = 0;
/**
* Contents buffer
* @var \SplFixedArray
*/
protected $buffer;
/**
* Size of the ring buffer
* @var int
*/
protected $size;
/**
* RingBuffer constructor
*
* Handles arguments through 'func_get_args' (gotta love PHP)
*
* @param int $size Size of the ring buffer
* @param array $val Initial values
*/
public function __construct()
{
//argument checks
$argv = func_get_args();
$argc = count($argv);
switch($argc)
{
case 1:
// array
if (is_array($argv[0]))
{
$this ->size = count($argv[0]);
$this->buffer = \SplFixedArray::fromArray($argv[0]);
}
// size
else if (is_numeric($argv[0]))
{
if ($argv[0] < 1)
{
throw new \InvalidArgumentException('RingBuffer ctor: size must be greater than zero');
}
$size = (integer)$argv[0];
$this->buffer = \SplFixedArray::fromArray(array_fill(0, $size, null));
$this->size = $size;
}
else
{
throw new \InvalidArgumentException("RingBuffer ctor: arguments must be an array ,a numeric size or both");
}
break;
case 2:
if(is_array($argv[0]) && is_numeric($argv[1]))
{
if ($argv[1] < 1)
{
throw new \InvalidArgumentException('RingBuffer ctor: size must be greater than zero');
}
$arr_size = count($argv[0]);
$size = (integer)$argv[1];
if ($arr_size == $size)
{
$this->buffer = \SplFixedArray::fromArray($argv[0]);
$this->size = $size;
}
else if ($arr_size > $size)
{
$this->buffer = \SplFixedArray::fromArray(array_slice($argv[0], 0, $size));
$this->size = $size ;
}
else // $arr_size < $size
{
$this->buffer = \SplFixedArray::fromArray(array_merge($argv[0], array_fill(0, $size - $arr_size, null)));
$this->size = $size ;
$this->position = $arr_size ;
}
}
else
{
throw new \InvalidArgumentException("RingBuffer ctor: arguments must be an array ,a numeric size or both");
}
break;
default:
throw new \InvalidArgumentException('RingBuffer ctor: no arguments given');
}
}
/**
* Returns the internal buffer as an array
*
* @return \SplFixedArray
*/
public function buffer()
{
return $this->buffer;
}
/**
* 'Countable' interface methods
*/
/**
* Returns the size of the buffer
*
* @return int
*/
public function count() : int
{
return $this->size;
}
/**
* 'ArrayAccess' interface methods
*/
protected function offsetOf($offset)
{
return ($this->position + $offset) % $this->size;
}
public function offsetExists($offset) : bool
{
return ($offset >= 0) && ($offset < $this->size);
}
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
// if (!$this->offsetExists($offset))
if (($offset < 1) && ($offset >= $this->size))
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
return $this->buffer[($this->position + $offset) % $this->size];
}
public function offsetUnset($offset) : void
{
if (!$this->offsetExists($offset))
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
$this->buffer[$this->offsetOf($offset)] = null;
}
public function offsetSet($offset, $value) : void
{
if ($offset === null)
{
$this->buffer[$this->position] = $value;
$this->position = ($this->position + 1)%$this->size;
}
else if ($this->offsetExists($offset))
{
$this->buffer[$this->offsetOf($offset)] = $value;
}
else
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
}
/**
* 'Iterator' interface methods
*/
public function rewind() : void
{
$this->iterator_position = 0;
}
public function current() : mixed
{
return $this->buffer[$this->offsetOf($this->iterator_position)];
}
public function key() : mixed
{
return $this->iterator_position;
}
public function next() : void
{
$this->iterator_position++;
}
public function valid() : bool
{
return ($this->iterator_position >= 0) && ($this->iterator_position < $this->size);
}
}
Parser/Token.php 0000644 00000002032 15235314576 0007601 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* Token
* Represents a single lexer token
*/
class Token
{
/**
* Token type
*
* @var string
*/
public $type;
/**
* The token's text
*
* @var string
*/
public $text;
/**
* Token position in the input stream
*
* @var integer
*/
public $position;
public function __construct($type, $text, $pos)
{
$this->type = $type;
$this->text = $text;
$this->position = $pos;
}
/**
* __toString magic method (for debugging)
*
* @return string
*/
public function __toString()
{
return '[' . $this->text .', ' . $this->type . ', ' . $this->position .']';
}
} Parser/ShortcodeParserHelper.php 0000644 00000031702 15235314576 0012776 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
use DateTime;
use DateTimeImmutable;
defined('_JEXEC') or die;
class ShortcodeParserHelper {
/**
* Input text buffer
*
* @var string
*/
protected $text;
/**
* ShortcodeParser options
*
* @var object
*/
protected $parser_options;
/**
* Parsing payload, associative array
*
* @var array
*/
protected $payload;
/**
* Parsing context
*
* @var string
*/
protected $context;
/**
* List of areas in the content that should not be parsed for Smart Tags.
*
* @var array
*/
private $protectedAreas = [];
/**
* @param string $text Text buffer (stored as a reference)
* @param object $parser_options ShortcodeParser options
* @param array|null $payload Parser payload
* @param string|null $context Parser context
*/
public function __construct(&$text, $payload = null, $parser_options = null, $context = null)
{
$this->text =& $text;
$this->payload = $payload;
$this->context = $context;
if (!$parser_options)
{
$this->parser_options = new \stdClass();
$this->parser_options->tag_open_char = '{';
$this->parser_options->tag_close_char = '}';
$this->parser_options->if_keyword = 'if';
$this->parser_options->log_errors = 'false';
}
else
{
$this->parser_options = $parser_options;
}
}
/**
* The text being parsed may contain sensitive information and areas where parsing of shortcodes, such as <script> tags, must be skipped.
* This method aids in protecting these areas by replacing the sensitive content with a hash, which can be restored later.
*
* @return void
*/
private function protectAreas()
{
$reg = '/<script[\s\S]*?>[\s\S]*?<\/script>/';
preg_match_all($reg, $this->text, $protectedAreas);
if (!$protectedAreas[0])
{
return;
}
foreach ($protectedAreas[0] as $protectedArea)
{
$hash = md5($protectedArea);
$this->protectedAreas[] = [$hash, $protectedArea];
$this->text = str_replace($protectedArea, $hash, $this->text);
}
}
/**
* Restore protected areas in the result text.
*
* @return void
*/
private function restoreProtectedAreas()
{
if (empty($this->protectedAreas))
{
return;
}
foreach ($this->protectedAreas as $protectedArea)
{
$this->text = str_ireplace($protectedArea[0], $protectedArea[1], $this->text);
}
}
/**
*
*/
public function parseAndReplace()
{
$this->protectAreas();
$replacements = [];
$shortcodes_text = [];
$shortcode_lexer = new ShortcodeLexer($this->text, $this->parser_options);
$shortcode_parser = new ShortcodeParser($shortcode_lexer, $this->parser_options);
$shortcodes = $shortcode_parser->expr();
foreach ($shortcodes as $shortcode)
{
// check if the shortcode has errors
if (\property_exists($shortcode, 'parser_error'))
{
// we cannot remove the shortcode at this point because it's 'content' could not be parsed
// error message is added before the shortode
$this->text = substr_replace($this->text, $shortcode->parser_error, $shortcode->position, 0);
continue;
}
$cond_lexer = new ConditionLexer(htmlspecialchars_decode($shortcode->conditions));
$cond_parser = new ConditionParser($cond_lexer);
// parse the shortcode's 'conditions' expression
$conditions = [];
try
{
$conditions = $cond_parser->expr();
// check if the shortcode has the correct context
// if ($conditions['context'] !== $this->context)
// {
// continue;
// }
// get the parsed logical operator (and/or)
$logic_op = array_key_exists('logic_op', $conditions) ? $conditions['logic_op'] : 'and';
// check if the debug param is set and we are logged in as a Super User
$debug_enabled = array_key_exists('debug', $conditions['global_params']) && $conditions['global_params']['debug'] &&
\Joomla\CMS\Factory::getUser()->authorise('core.admin');
// check for the noPrepareContent global param
$prepare_content = !array_key_exists('nopreparecontent', $conditions['global_params']);
// evaluate conditions
$evaluator = new ConditionsEvaluator($conditions['conditions'], $this->payload, $debug_enabled);
$results = $evaluator->evaluate();
// get the final result
$pass = $logic_op === 'and' && !empty($results);
foreach($results as $result)
{
if ($logic_op === 'and')
{
$pass &= $result['pass'];
}
else
{
$pass |= $result['pass'];
}
}
//
$replacement = $shortcode_parser->getReplacement($shortcode->content, $pass);
if ($debug_enabled)
{
list($content, $alt_content) = $shortcode_parser->getReplacement($shortcode->content, null);
$replacement .= $this->prepareDebugInfo($shortcode->conditions, $conditions, $results, $pass, $content, $alt_content);
}
}
catch (\Exception $error)
{
// Log the error and remove the shortcode from the input text
$replacement = $error->getMessage();
}
// fire the onContentPrepare event for the replacement text
// if ($prepare_content)
// {
// $replacement = \Joomla\CMS\HTML\Helpers\Content::prepare($replacement);
// }
// store 'replacement' text
$replacements[] = $replacement;
// store the original shortcode text
$shortcodes_text[] = substr($this->text, $shortcode->start, $shortcode->length);
}
// replace all shortcodes
$this->replaceContent($shortcodes_text, $replacements);
$this->restoreProtectedAreas();
}
/**
* Performs content replacement in the input text buffer
*
* @param array $shortcodes_text Array containing the shortcodes text
* @param array $replacements Array containng the shortcode replacements
* @param array $debug_info Contains debug info for each shortcode
* @return void
*/
protected function replaceContent($shortcodes_text, $replacements)
{
$this->text = \str_replace($shortcodes_text, $replacements, $this->text);
}
/**
*
*/
protected function prepareDebugInfo($conditions_text, $conditions, $results, $pass, $content, $alt_content)
{
$format_value = function($value)
{
if (is_array($value))
{
return implode(', ', $value);
}
if ($value instanceof \DateTime || $value instanceof \DateTimeImmutable)
{
return $value->format(\DateTimeInterface::RFC1036);
}
return $value;
};
$format_condition = function($conditions) use ($results, $format_value, $conditions_text)
{
$res = [];
foreach ($conditions['conditions'] as $idx => $condition)
{
$params = count($condition['params']) ?
array_reduce(array_keys($condition['params']), function($acc, $key) use($condition) {
return $acc . "{$key}: " . $condition['params'][$key] . "<br>";
}, '') :
null;
$res[] = [
'title' => ($condition['alias'] ?? $results[$idx]['l_func_name'] . '('. $format_value($results[$idx]['l_func_args']) . ') ') . ($results[$idx]['pass'] ? '<span style="color: green;"> ✓</span>' : '<span style="color: red;"> ✗</span>'),
'body' => '<ul style="margin-bottom: 0">' . implode('',
array_filter([
'<li>Condition Operand: ' . (array_key_exists('l_func_name', $results[$idx]) ? $format_value($results[$idx]['l_func_val']) : $format_value($results[$idx]['actual_value'])) .
(array_key_exists('l_eval', $results[$idx]) && ($results[$idx]['l_eval'] != $results[$idx]['l_func_val']) ? ' (evaluated as "' . $format_value($results[$idx]['l_eval']) . '")': '') .'</li>',
$condition['values'] ? '<li>Value Operand: ' . $format_value($condition['values']) .
(array_key_exists('r_evbal', $results[$idx]) && ($results[$idx]['r_eval'] != $condition['values']) ? ' (evaluated as "' . $format_value($results[$idx]['r_eval']) .'")' : '') . '</li>' : null,
array_key_exists('r_func_name', $results[$idx]) ? '<li>Value Operand: ' . $results[$idx]['r_func_name'] . '('. $format_value($results[$idx]['r_func_args']) . '): ' . $format_value($results[$idx]['r_func_val']) .'</li>': null,
'<li>Operator: ' . $this->operatorToString($condition['operator']) .'</li>',
$params ? '<li style="list-style-type: none; margin-left: -1rem;">' . $this->debugInfoHTML(['title' => 'Parameters', 'body' => $params]) . '</li>': null
])
) . '</ul>'
];
}
return $res;
};
$title = $pass ? '<span style="color: green;"> ✓</span>' : '<span style="color: red;"> ✗</span>';
$info = [
'title' => str_replace('--debug', '', $conditions_text) . ' ' . $title,
'body' => '',
'children' => array_filter([
['title' => 'Conditions', 'body' => '', 'children' => $format_condition($conditions)],
count($conditions['conditions']) > 1 ? ['title' => 'Logical Operator: ' . $conditions['logic_op'], 'children' => []] : null,
['title' => 'Content', 'body' => $content, 'children' => []],
!empty($alt_content) ? ['title' => 'Alt. Content', 'body' => $alt_content, 'children' => []] : null
])
];
return '<div style="display: flex; justify-content: center; text-align: start;">' . $this->debugInfoHTML($info) . '</div>';
}
/**
*
*/
protected function debugInfoHTML($info)
{
$title = \array_key_exists('title', $info) ? $info['title'] : '';
$body = \array_key_exists('body', $info) ? $info['body'] : '';
$children = '';
if (\array_key_exists('children', $info))
{
foreach ($info['children'] as $c)
{
$children .= $this->debugInfoHTML($c);
}
}
return '<details style=""><summary style="cursor: pointer; ">' . $title . '</summary><div style="margin: 0.2em 0.5em;">' . $body . $children . '</div></details>';
}
/**
* Converts shortcode operators to a human readable string
*
*/
protected function operatorToString($op)
{
switch($op)
{
case 'equals':
return 'equals';
case 'starts_with':
return 'startsWith';
case 'ends_with':
return 'endsWith';
case 'contains':
return 'contains';
case 'contains_any':
return 'containsAny';
case 'contains_all':
return 'containsAll';
case 'contains_only':
return 'containsOnly';
case 'lt':
return 'lessThan';
case 'lte':
return 'lessThanEquals';
case 'gt':
return 'greaterThan';
case 'gte':
return 'greaterThanEquals';
case 'empty':
return 'empty';
default:
throw new Exceptions\UnknownOperatorException($op);
}
}
} Parser/ShortcodeParser.php 0000644 00000015226 15235314576 0011641 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Parser;
use NRFramework\Parser\ShortcodeLexer;
/**
* ShortcodeParser
* LL(k = 3) recursive-decent parser
* Uses ShortcodeLexer as the input/token source
*
* Parses the following grammar:
* ------------------------------
* expr := shortcode* <-- Top-level expression (i.e. 0 or more shortcodes)
* shortcode := ifexpr content endifexpr
* ifexpr := {sc_open} {if_keyword} condition {sc_close}
* endifexpr := {sc_open} {end_ifkeyword} {sc_close}
* content := any text until endifexpr
* condition := any text with preserved quoted values until {sc_close}
*/
class ShortcodeParser extends Parser
{
/**
* shortcode opening character (e.g.: '{')
* @var string
*/
protected $sc_open;
/**
* shortcode closing character (e.g.: '}')
* @var string
*/
protected $sc_close;
/**
* Log parsing errors?
*
* @var boolean
*/
protected $log_errors;
/**
* The shortcode's position in the input text
* Used only for error logging
*
* @var int
*/
protected $shortcode_position;
/**
* Constructor
*
* @param ShortcodeLexer $input
* @param Object $options
*/
public function __construct(ShortcodeLexer $input, $options = null)
{
// k = 3, look 3 tokens ahead at most
parent::__construct($input, 3);
$this->sc_open = $options->tag_open_char ?? '{';
$this->sc_close = $options->tag_close_char ?? '}';
$this->log_errors = $options->log_errors ?? false;
$this->shortcode_position = $options->shortcode_position ?? 0;
}
/**
* Returns the correct content for replacement
* If $pass = null will return an array with [content, else-content]
*
* Call this method when the conditions have been parsed and
* the result is known
*
* @param string $content
* @param bool $pass
* @return string
*/
public function getReplacement($content, $pass)
{
//construct the else-tag, e.g. {else}
$elseTag = $this->sc_open . 'else' . $this->sc_close;
// split content on the else-tag
$replacement = $content;
$elseReplacement = '';
if (strpos($content, $elseTag) !== false)
{
list($replacement, $elseReplacement) = explode($elseTag, $content, 2);
}
return $pass === null ?
[$replacement, $elseReplacement] :
($pass ? $replacement : $elseReplacement);
}
/**
* Top-level parsing method
*
* Rule:
* expr := shortcode*
*
* @return array
*/
public function expr()
{
$shortcodes = [];
while ($this->lookahead[0]->type != 'EOF')
{
$position = $this->lookahead[0]->position;
try
{
if ($this->lookahead[0]->type == 'sc_open' &&
$this->lookahead[1]->type == 'if_keyword')
{
$shortcodes[] = $this->shortcode();
}
else
{
// this token is not part of a shortcode, keep going...
$this->consume();
}
}
catch (\Exception $error)
{
// something went horribly wrong while parsing a shortcode
// log the error and continue
$msg = $error->getMessage();
$near_text = $this->lookahead[0]->position + $this->shortcode_position;
$shortcodes[] = (object) [
'position' => $position,
'parser_error' => $msg,
'near_text' => $near_text
];
$this->consume();
}
}
return $shortcodes;
}
/**
* Rule
*
* shortcode := ifexpr content endifexpr
*
* @return object
*/
protected function shortcode()
{
$start = $this->lookahead[0]->position + $this->shortcode_position;
$conditions = $this->ifexpr();
$content = $this->content();
$length = $this->lookahead[2]->position + $this->shortcode_position - $start + 1;
$this->endifexpr();
return (object) [
'start' => $start,
'length' => $length,
'conditions' => $conditions,
'content' => $content
];
}
/**
* Rule:
* ifexpr : {sc_open} {if_keyword} condition {sc_close}
*
* @return string
*/
protected function ifexpr()
{
$this->match('sc_open');
$this->match('if_keyword');
$condition_text = $this->condition();
$this->match('sc_close');
return $condition_text;
}
/**
* Rule:
* endifexpr := {sc_open} {end_ifkeyword} {sc_close}
*
* @return void
*/
protected function endifexpr()
{
$this->match('sc_open');
$this->match('endif_keyword');
$this->match('sc_close');
}
/**
* Rule:
* condition := any text with preserved quoted values until {sc_close}
*
* @return string
* @throws Exception
*/
protected function condition()
{
$buf = '';
while ($this->lookahead[0]->type !== 'EOF')
{
if ($this->lookahead[0]->type === 'sc_close')
{
return htmlspecialchars_decode($buf);
}
$buf .= $this->lookahead[0]->text;
$this->consume();
}
throw new Exceptions\SyntaxErrorException('Invalid condition expression.');
}
/**
* Rule:
* content := any text until an endif expression
*
* @return string
* @throws Exception
*/
protected function content()
{
$buf = '';
while ($this->lookahead[0]->type !== 'EOF')
{
if ($this->lookahead[0]->type === 'sc_open' &&
$this->lookahead[1]->type === 'endif_keyword' &&
$this->lookahead[2]->type === 'sc_close')
{
return $buf;
}
$buf .= $this->lookahead[0]->text;
$this->consume();
}
throw new Exceptions\SyntaxErrorException('Missing shortcode tag character.');
}
}
Parser/ConditionLexer.php 0000644 00000037407 15235314576 0011465 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
/**
* ConditionLexer
*
* Tokens:
* -------
* and : 'AND'
* or : 'OR'
* quotedval : quotes ~(quotes)* quotes
* literal : ~(whitespace | quotes)+
* ident : ('a'..'z' | 'A'..'Z' | '_' | '\-' | '\.')+
* quotes : '\'' | '\"'
* comma : ','
* l_paren : '('
* r_paren : ')'
*
* negate_op : '!'
* equals : '=' | 'equals'
* contains : '*=' | 'contains'
* contains_any : 'containsAny'
* contains_all : 'containsAll'
* contains_only : 'containsOnly'
* ends_with : '$=' | 'endsWith'
* starts_with : '^=' | 'startsWith'
* lt : '<' | 'lt' | 'lowerThan'
* lte : '<=' | 'lte' | 'lowerThanEqual'
* gt : '>' | 'gt' | 'greaterThan'
* gte : '>=' | 'gte' | 'greaterThanEqual'
* empty : 'empty'
*
* param : '--' . ident
* whitespace : ' ' | '\r' | '\n' | '\t'
*/
class ConditionLexer extends Lexer
{
/**
* ConditionLexer constructor
*
* @param string $input
*/
public function __construct($input)
{
parent::__construct($input);
// single char tokens
$this->tokens->addType('comma');
$this->tokens->addType('quote');
$this->tokens->addType('dquote');
$this->tokens->addType('l_paren');
$this->tokens->addType('r_paren');
// operators
$this->tokens->addType('negate_op');
$this->tokens->addType('equals');
$this->tokens->addType('contains');
$this->tokens->addType('contains_all');
$this->tokens->addType('contains_any');
$this->tokens->addType('contains_only');
$this->tokens->addType('ends_with');
$this->tokens->addType('starts_with');
$this->tokens->addType('lt');
$this->tokens->addType('gt');
$this->tokens->addType('lte');
$this->tokens->addType('gte');
$this->tokens->addType('empty');
// logical operators
$this->tokens->addType('and');
$this->tokens->addType('or');
// values/literals/identifiers/parameters
$this->tokens->addType('quotedvalue');
$this->tokens->addType('literal');
$this->tokens->addType('ident');
$this->tokens->addType('param');
}
/**
* Returns the next token from the input string
*
* @return NRFramework\Parser\Token
* @throws Exception
*/
public function nextToken()
{
while ($this->cur !== Lexer::EOF)
{
if (preg_match('/\s+/', $this->cur))
{
$this->whitespace();
continue;
}
switch ($this->cur)
{
// match tokens from single char predictions
case ',':
return $this->comma();
case "'":
return $this->quotedValue("'");
case '"':
return $this->quotedValue('"');
case '=':
return $this->equals();
case '!':
return $this->negate_op();
case '*':
return $this->contains();
case '$':
return $this->ends_with();
case '^':
return $this->starts_with();
case '<':
return $this->lt_or_lte();
case '>':
return $this->gt_or_gte();
case '(':
return $this->l_paren();
case ')':
return $this->r_paren();
case '-':
$this->mark();
$next_chars = $this->consume(2);
if ($next_chars === '--')
{
$this->reset();
return $this->param();
}
$this->reset();
// match other tokens
default:
if (!$this->isValidChar())
{
throw new Exceptions\SyntaxErrorException('Invalid character: ' . $this->cur);
}
$token = null;
// try to match literal operators
$token = $this->literal_ops();
if($token)
{
return $token;
}
// try to match boolean operators
$token = $this->_and();
if($token)
{
return $token;
}
$token = $this->_or();
if($token)
{
return $token;
}
// if we get here the token is certainly a literal
$pos = $this->index;
$token = $this->literal();
if ($token)
{
// check if the literal also qualifies to be an identifier
if ($this->isValidIdentifier($token->text))
{
$token = $this->tokens->create('ident', $token->text, $pos);
}
return $token;
}
return null;
}
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* Checks if a string qualifies to be an identifier
*
* @return bool
*/
protected function isValidIdentifier($text)
{
$ident_regex = '/(^[a-zA-Z\_]{1}$)|(^[a-zA-Z\_](?=([\w\-\.]*))([\w\-\.]*))/';
return preg_match($ident_regex, $text);
}
/**
* Check if the current character is valid for
* some matching rules (and, or, literal, ident)
*
* @return boolean
*/
protected function isValidChar()
{
$r = '/[^\s\'\",=\!\(\)\~\*\<\>\$\^]/';
return preg_match($r, $this->cur);
}
/**
* literal : ~(whitespace | quotes)+ //one or more chars except whitespace and quotes
*
* @return Token|void
*/
protected function literal()
{
$pos = $this->index;
$buf = '';
do
{
if (!$this->isValidChar())
{
break;
}
$buf .= $this->cur;
$this->consume();
}
while ($this->cur !== Lexer::EOF);
if (strlen($buf) > 0)
{
return $this->tokens->create('literal', $buf, $pos);
}
}
/**
* and : 'AND'
*
* @return Token|void
*/
protected function _and()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if (preg_match('/and/', strtolower($buf)))
{
return $this->tokens->create('and', trim($buf), $pos);
}
$this->reset();
}
/**
* or : 'OR'
*
* @return Token|void
*/
public function _or()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if (preg_match('/or/', strtolower($buf)))
{
return $this->tokens->create('or', trim($buf), $pos);
}
$this->reset();
}
/**
* quotedval : quotes ~(quotes)* quotes
*
* @return Token|void
* @throws Exception
*/
protected function quotedValue($q)
{
$pos = $this->index;
$otherQuote = $q === '"' ? "'" : '"';
$quote_queue = [];
$buf = '';
$quote_queue[] = $q;
$this->consume();
while (!empty($quote_queue))
{
if ($this->cur === Lexer::EOF)
{
throw new Exceptions\SyntaxErrorException('Missing quote at: ' . $buf);
}
if ($this->cur === end($quote_queue))
{
array_pop($quote_queue);
// if it's not the opening quote
if (!empty($quote_queue))
{
$buf .= $this->cur;
}
}
else if ($this->cur === $otherQuote)
{
array_push($quote_queue, $otherQuote);
$buf .= $otherQuote;
}
else
{
$buf .= $this->cur;
}
$this->consume();
}
return $this->tokens->create('quotedvalue', $buf, $pos);
}
/**
* param : '--' . ident
*
* @return Token|void
*/
protected function param()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '--')
{
$buf = '';
do
{
if (!$this->isValidChar())
{
break;
}
$buf .= $this->cur;
$this->consume();
}
while ($this->cur !== Lexer::EOF);
if (strlen($buf) > 0 && $this->isValidIdentifier($buf))
{
return $this->tokens->create('param', $buf, $pos);
}
}
$this->reset();
}
/**
* equals : '='
*
* @return Token|void
*/
protected function equals()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('equals', "=", $pos);
}
protected function negate_op()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('negate_op', "!", $pos);
}
/**
* comma : ','
*
* @return Token
*/
protected function comma()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('comma', ",", $pos);
}
/**
* l_paren : '('
*/
protected function l_paren()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('l_paren', '(', $pos);
}
/**
* r_paren : ')'
*/
protected function r_paren()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('r_paren', ')', $pos);
}
/**
* contains: '*='
*
* @return Token|void
*/
protected function contains()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '*=')
{
return $this->tokens->create('contains', "*=", $pos);
}
$this->reset();
}
/**
* contains_word: '~='
*
* @return Token|void
*/
protected function contains_word()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '~=')
{
return $this->tokens->create('contains_word', "~=", $pos);
}
$this->reset();
}
/**
* ends_with: '$='
*
* @return Token|void
*/
protected function ends_with()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '$=')
{
return $this->tokens->create('ends_with', "$=", $pos);
}
$this->reset();
}
/**
* starts_with: '$='
*
* @return Token|void
*/
protected function starts_with()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '^=')
{
return $this->tokens->create('starts_with', "^=", $pos);
}
$this->reset();
}
/**
* lt_or_lte: '<' | '<='
*
* @return Token|void
*/
protected function lt_or_lte()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '<=')
{
return $this->tokens->create('lte', "<=", $pos);
}
else
{
$this->reset();
$this->consume();
return $this->tokens->create('lt', '<', $pos);
}
$this->reset();
}
/**
* gt_or_gte: '>' | '>='
*
* @return Token|void
*/
protected function gt_or_gte()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '>=')
{
return $this->tokens->create('gte', ">=", $pos);
}
else
{
$this->reset();
$this->consume();
return $this->tokens->create('gt', '>', $pos);
}
$this->reset();
}
/**
* Literal Operators predictor
*
* @return Token|null
*/
protected function literal_ops()
{
$pos = $this->index;
$this->mark();
$lit = $this->literal();
if ($lit)
{
switch (strtolower($lit->text))
{
case 'equals':
return $this->tokens->create('equals', $lit->text, $pos);
case 'startswith':
return $this->tokens->create('starts_with', $lit->text, $pos);
case 'endswith':
return $this->tokens->create('ends_with', $lit->text, $pos);
case 'contains':
return $this->tokens->create('contains', $lit->text, $pos);
case 'containsall':
return $this->tokens->create('contains_all', $lit->text, $pos);
case 'containsany':
return $this->tokens->create('contains_any', $lit->text, $pos);
case 'containsonly':
return $this->tokens->create('contains_only', $lit->text, $pos);
case 'lt':
case 'lowerthan':
return $this->tokens->create('lt', $lit->text, $pos);
case 'lte':
case 'lowerthanequal':
return $this->tokens->create('lte', $lit->text, $pos);
case 'gt':
case 'greaterthan':
return $this->tokens->create('gt', $lit->text, $pos);
case 'gte':
case 'greaterthantequal':
return $this->tokens->create('gte', $lit->text, $pos);
case 'empty':
return $this->tokens->create('empty', $lit->text, $pos);
}
}
$this->reset();
}
}
Parser/Parser.php 0000644 00000006506 15235314576 0007767 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
use NRFramework\Parser\RingBuffer;
/**
* Parser base class
* LL(k) recursive-decent parser with backtracking support
*/
abstract class Parser
{
/**
* Lexer instance (feeds the parser with tokens)
*
* @var NRFramework\Parser\Lexer
*/
protected $input = null;
/**
* Ring buffer of the next k tokens
* from the input stream
*
* @var RingBuffer
*/
protected $lookahead = null;
/**
* k: Number of lookahead tokens
*
* @var int
*/
protected $k;
/**
* Array(stack) containing the current
* contents of the lookahead buffer when
* marking the position of the stream
*
* @var array
*/
protected $lookahead_history = null;
/**
* Lexer constructor
*
* @param Lexer $input
* @param integer $k, number of lookahead tokens
*/
public function __construct(Lexer $input, $k = 1)
{
if (!is_integer($k) || ($k < 1))
{
throw new \InvalidArgumentException('Parser: $k must be greater than 0!');
}
$this->k = $k;
$this->input = $input;
$this->lookahead_history = [];
// initialize lookahead buffer
$this->resetBuffer();
}
/**
* Checks the type of the next token.
* Advances the position in the token stream.
*
* @param string $type
* @return void
*
* @throws Exception
*/
public function match($type)
{
if ($this->lookahead[0]->type === $type)
{
$this->consume();
return;
}
throw new Exceptions\SyntaxErrorException('Expecting token ' . $type . ', found ' . $this->lookahead[0]);
}
/**
* Retrieves the next token from the input stream
* and add it to the buffer.
*
* @return void
*/
public function consume()
{
$this->lookahead[] = $this->input->nextToken();
}
/**
* Marks the position in the token stream
*
* @return void
*/
public function mark()
{
array_push($this->lookahead_history, $this->lookahead);
$this->input->mark();
}
/**
* Reset to a previously marked position
* in the token stream
*
* @return void
*/
public function reset()
{
$this->input->reset();
// reset lookahead buffer if not marked
if (empty($this->lookahead_history))
{
$this->resetBuffer();
}
// normal reset
else
{
$this->lookahead = array_pop($this->lookahead_history);
}
}
/**
* Resets and refills the lookahead buffer starting
* from the current position in the token stream
*
* @return void
*/
protected function resetBuffer()
{
$this->lookahead = new RingBuffer($this->k);
for ($i=0; $i < $this->k; $i++)
{
$this->consume();
}
}
}
Parser/Exceptions/UnknownOperatorException.php 0000644 00000000774 15235314576 0015707 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnknownOperatorException extends \Exception
{
public function __construct($operator)
{
parent::__construct("003 - Unknown Comparison Operator: " . $operator);
}
} Parser/Exceptions/UnsupportedValueOperandException.php 0000644 00000001263 15235314576 0017364 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnsupportedValueOperandException extends \Exception
{
public function __construct($operator, $accepts_multi_values)
{
$message = 'The Comparison Operator "' . $operator . '" can only be used with ' . ($accepts_multi_values ? 'multiple values.' : 'single values.');
parent::__construct("006 - Unsupported Value Operand: " . $message);
}
} Parser/Exceptions/UnsupportedOperatorException.php 0000644 00000001345 15235314576 0016573 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnsupportedOperatorException extends \Exception
{
public function __construct($operator, $condition_name, $accepts_multi_values)
{
$message = 'The Comparison Operator "' . $operator . '" can only be used with Condition Operands that return ' . ($accepts_multi_values ? 'multiple values.' : 'single values.');
parent::__construct("005 - Unsupported Comparison Operator: " . $message);
}
} Parser/Exceptions/InvalidConditionException.php 0000644 00000001044 15235314576 0015760 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class InvalidConditionException extends \Exception
{
public function __construct($condition_name)
{
parent::__construct("002 - Invalid Condition: The condition '" . $condition_name . "' does not exist.");
}
} Parser/Exceptions/ConditionValueException.php 0000644 00000001057 15235314576 0015452 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class ConditionValueException extends \Exception
{
public function __construct($condition_name)
{
parent::__construct("008 - Condition Value Error: The Condition '" . $condition_name . "' does not return a value.");
}
} Parser/Exceptions/SyntaxErrorException.php 0000644 00000000747 15235314576 0015034 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class SyntaxErrorException extends \Exception
{
public function __construct($message)
{
parent::__construct("001 - Syntax Error: " . $message);
}
} Parser/Exceptions/UnknownFunctionException.php 0000644 00000000763 15235314576 0015677 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnknownFunctionException extends \Exception
{
public function __construct($func_name)
{
parent::__construct("007 - Unknown Function: " . $func_name);
}
} Helpers/File.php 0000644 00000002622 15235314576 0007553 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use Joomla\CMS\Uri\Uri;
class File
{
public static function getFileSources($sources, $allowedExtensions = null)
{
if (!$sources)
{
return;
}
// Support comma separated values
$sources = is_array($sources) ? $sources : explode(',', $sources);
$result = [];
$ds = DIRECTORY_SEPARATOR;
foreach ($sources as $source)
{
if (!$pathinfo = pathinfo($source))
{
continue;
}
if (!isset($pathinfo['extension']))
{
continue;
}
if ($allowedExtensions && !in_array($pathinfo['extension'], $allowedExtensions))
{
continue;
}
// Add root path to local source
if (strpos($source, 'http') === false)
{
$source = Uri::root() . ltrim($source, '/');
}
$result[] = [
'ext' => $pathinfo['extension'],
'file' => $source
];
}
return $result;
}
} Helpers/Video.php 0000644 00000003756 15235314576 0007753 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
class Video
{
/**
* Returns the Video URL details.
*
* Supported platforms:
* - YouTube
* - Vimeo
*
* @param string $url
*
* @return array
*/
public static function getDetails($url)
{
$id = '';
$provider = '';
if (preg_match(self::getYouTubePattern(), $url, $matches))
{
$id = !empty($matches[1]) ? $matches[1] : $matches[2];
$provider = 'youtube';
}
else if (preg_match(self::getVimeoPattern(), $url, $matches))
{
$id = !empty($matches[1]) ? $matches[1] : null;
$provider = 'vimeo';
}
else if (preg_match(self::getFacebookVideoPattern(), $url))
{
$id = $url;
$provider = 'facebookvideo';
}
else if (preg_match(self::getDailymotionPattern(), $url, $matches))
{
$id = end($matches);
$provider = 'dailymotion';
}
return [
'id' => $id,
'provider' => $provider
];
}
/**
* Get YouTube Pattern.
*
* @return string
*/
public static function getYouTubePattern()
{
return '/^https?:\/\/(?:m\.|www\.)?youtube\.com\/(?:watch\?v=|embed\/)?([a-zA-Z0-9_-]{11})|^https?:\/\/youtu\.be\/([a-zA-Z0-9_-]{11})/';
}
/**
* Get Vimeo Pattern.
*
* @return string
*/
public static function getVimeoPattern()
{
return '/^https?:\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(\d+)/';
}
/**
* Get Facebook Video Pattern.
*
* @return string
*/
public static function getFacebookVideoPattern()
{
return '/^(?:(?:https?:)?\/\/)?(?:www\.)?facebook\.com\/(?:watch\/\?v=|[\w\.]+\/videos\/(?:[\w\.]+\/)?)?(\d+)/';
}
/**
* Get Dailymotion Pattern.
*
* @return string
*/
public static function getDailymotionPattern()
{
return '/(?:dailymotion\.com\/(?:video|hub)\/|dai\.ly\/)([a-zA-Z0-9]+)/';
}
} Helpers/CSS.php 0000644 00000001504 15235314576 0007322 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
class CSS
{
/**
* Transforms an array of CSS variables (key, value) to
* a CSS output.
*
* @param array $cssVars
* @param string $namespace
*
* @return string
*/
public static function cssVarsToString($cssVars, $namespace)
{
$output = '';
foreach (array_filter($cssVars) as $key => $value)
{
$output .= '--' . $key . ': ' . $value . ';' . "\n";
}
return $namespace . ' {
' . $output . '
}
';
}
} Helpers/Responsive.php 0000644 00000017262 15235314576 0011037 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use NRFramework\Cache;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\PluginHelper;
class Responsive
{
/**
* Renders the given CSS.
*
* @param array $css
* @param string $selector
*
* @return string
*/
public static function renderResponsiveCSS($css, $selector = '')
{
if (!$css || !is_array($css))
{
return;
}
$output = '';
foreach (self::getBreakpoints() as $breakpoint => $breakpoint_data)
{
if (!isset($css[$breakpoint]) || empty($css[$breakpoint]))
{
continue;
}
/**
* If we were given an array of strings of CSS, transform them to a string so we can output it.
*
* i.e. transform
* [
* 'color: #fff;',
* 'background: #000;'
* ]
*
* to:
*
* 'color: #fff;background: #000;'
*/
if (!is_string($css[$breakpoint]))
{
$css[$breakpoint] = implode('', $css[$breakpoint]);
}
$max_width = isset($breakpoint_data['max_width']) ? $breakpoint_data['max_width'] : '';
$output .= self::getGenericTemplate($css[$breakpoint], $max_width, $selector);
}
return $output;
}
/**
* Returns the responsive output of a specific media query size.
*
* @param string $css The Custom CSS
* @param int $size This is the max-width in pixels
* @param string $selector The CSS Selector to apply the CSS
*
* @return string
*/
public static function getGenericTemplate($css, $size = '', $selector = '')
{
if (!is_string($css) || !is_scalar($size) || !is_string($selector))
{
return '';
}
$selector_prefix = $selector_suffix = $size_prefix = $size_suffix = '';
if (!empty($size))
{
$size_prefix = '@media screen and (max-width: ' . $size . 'px){';
$size_suffix = '}';
}
if (!empty($selector))
{
$selector_prefix = $selector . '{';
$selector_suffix = '}';
}
return $size_prefix . $selector_prefix . $css . $selector_suffix . $size_suffix;
}
/**
* Returns all breakpoints.
*
* @return array
*/
public static function getBreakpoints()
{
$breakpointsSettings = self::getBreakpointsSettings();
$tablet_max_width = isset($breakpointsSettings['tablet']) && !empty($breakpointsSettings['tablet']) ? $breakpointsSettings['tablet'] : 1024;
$mobile_max_width = isset($breakpointsSettings['mobile']) && !empty($breakpointsSettings['mobile']) ? $breakpointsSettings['mobile'] : 575;
return [
'desktop' => [
'icon' => '<svg width="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="mask0_112_458" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#D9D9D9"/></mask><g mask="url(#mask0_112_458)"><path d="M8.5 20.5V19H10.5V17H4.3077C3.80257 17 3.375 16.825 3.025 16.475C2.675 16.125 2.5 15.6974 2.5 15.1923V5.3077C2.5 4.80257 2.675 4.375 3.025 4.025C3.375 3.675 3.80257 3.5 4.3077 3.5H19.6923C20.1974 3.5 20.625 3.675 20.975 4.025C21.325 4.375 21.5 4.80257 21.5 5.3077V15.1923C21.5 15.6974 21.325 16.125 20.975 16.475C20.625 16.825 20.1974 17 19.6923 17H13.5V19H15.5V20.5H8.5ZM4.3077 15.5H19.6923C19.7692 15.5 19.8397 15.468 19.9038 15.4039C19.9679 15.3398 20 15.2692 20 15.1923V5.3077C20 5.23077 19.9679 5.16024 19.9038 5.09613C19.8397 5.03203 19.7692 4.99998 19.6923 4.99998H4.3077C4.23077 4.99998 4.16024 5.03203 4.09613 5.09613C4.03202 5.16024 3.99998 5.23077 3.99998 5.3077V15.1923C3.99998 15.2692 4.03202 15.3398 4.09613 15.4039C4.16024 15.468 4.23077 15.5 4.3077 15.5Z" fill="currentColor"/></g></svg>',
'label' => Text::_('NR_DESKTOP'),
'desc' => Text::_('NR_DESKTOPS_WITH_BREAKPOINT_INFO')
],
'tablet' => [
'icon' => '<svg width="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="mask0_112_446" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#D9D9D9"/></mask><g mask="url(#mask0_112_446)"><path d="M12 20.2692C12.2448 20.2692 12.4535 20.183 12.6259 20.0105C12.7984 19.8381 12.8846 19.6294 12.8846 19.3846C12.8846 19.1397 12.7984 18.9311 12.6259 18.7586C12.4535 18.5862 12.2448 18.5 12 18.5C11.7551 18.5 11.5465 18.5862 11.374 18.7586C11.2016 18.9311 11.1154 19.1397 11.1154 19.3846C11.1154 19.6294 11.2016 19.8381 11.374 20.0105C11.5465 20.183 11.7551 20.2692 12 20.2692ZM5.3077 22.5C4.80898 22.5 4.38302 22.3233 4.02982 21.9701C3.67661 21.6169 3.5 21.191 3.5 20.6923V3.3077C3.5 2.80898 3.67661 2.38302 4.02982 2.02982C4.38302 1.67661 4.80898 1.5 5.3077 1.5H18.6923C19.191 1.5 19.6169 1.67661 19.9701 2.02982C20.3233 2.38302 20.5 2.80898 20.5 3.3077V20.6923C20.5 21.191 20.3233 21.6169 19.9701 21.9701C19.6169 22.3234 19.191 22.5 18.6923 22.5L5.3077 22.5ZM4.99997 17.7692V20.6923C4.99997 20.782 5.02883 20.8557 5.08653 20.9134C5.14423 20.9711 5.21795 21 5.3077 21H18.6923C18.782 21 18.8557 20.9711 18.9134 20.9134C18.9711 20.8557 19 20.782 19 20.6923V17.7692H4.99997ZM4.99997 16.2692H19V5.74995H4.99997V16.2692ZM4.99997 4.25H19V3.3077C19 3.21795 18.9711 3.14423 18.9134 3.08652C18.8557 3.02882 18.782 2.99998 18.6923 2.99998H5.3077C5.21795 2.99998 5.14423 3.02882 5.08653 3.08652C5.02883 3.14423 4.99997 3.21795 4.99997 3.3077V4.25Z" fill="currentColor"/></g></svg>',
'label' => Text::_('NR_TABLET'),
'desc' => Text::sprintf('NR_TABLETS_WITH_BREAKPOINT_INFO', $tablet_max_width),
'max_width' => $tablet_max_width
],
'mobile' => [
'icon' => '<svg width="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="mask0_112_452" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"><rect width="24" height="24" fill="#D9D9D9"/></mask><g mask="url(#mask0_112_452)"><path d="M7.3077 22.4999C6.80257 22.4999 6.375 22.3249 6.025 21.9749C5.675 21.6249 5.5 21.1974 5.5 20.6923V3.3077C5.5 2.80257 5.675 2.375 6.025 2.025C6.375 1.675 6.80257 1.5 7.3077 1.5H16.6922C17.1974 1.5 17.625 1.675 17.975 2.025C18.325 2.375 18.5 2.80257 18.5 3.3077V20.6923C18.5 21.1974 18.325 21.625 17.975 21.975C17.625 22.325 17.1974 22.5 16.6922 22.5L7.3077 22.4999ZM6.99997 19.75V20.6923C6.99997 20.7692 7.03202 20.8397 7.09613 20.9039C7.16024 20.968 7.23077 21 7.3077 21H16.6922C16.7692 21 16.8397 20.968 16.9038 20.9039C16.9679 20.8397 17 20.7692 17 20.6923V19.75H6.99997ZM6.99997 18.25H17V5.74998H6.99997V18.25ZM6.99997 4.25003H17V3.30773C17 3.23079 16.9679 3.16027 16.9038 3.09615C16.8397 3.03205 16.7692 3 16.6922 3H7.3077C7.23077 3 7.16024 3.03205 7.09613 3.09615C7.03202 3.16027 6.99997 3.23079 6.99997 3.30773V4.25003Z" fill="currentColor"/></g></svg>',
'label' => Text::_('NR_MOBILE'),
'desc' => Text::sprintf('NR_MOBILES_WITH_BREAKPOINT_INFO', $mobile_max_width),
'max_width' => $mobile_max_width
]
];
}
public static function getBreakpointsSettings()
{
$hash = 'tassosResponsiveBreakpoints';
if (Cache::has($hash))
{
return Cache::get($hash);
}
$settings = PluginHelper::getPlugin('system', 'nrframework');
$default = [
'desktop' => 'any',
'tablet' => 1024,
'mobile' => 575
];
if (!isset($settings->params))
{
return $default;
}
if (!$params = json_decode($settings->params, true))
{
return $default;
}
$data = isset($params['breakpoints']) ? $params['breakpoints'] : [];
return Cache::set($hash, $data);
}
} Helpers/ChainedFields.php 0000644 00000006124 15235314576 0011357 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
class ChainedFields
{
/**
* Loads a combined array of the inputs and choices of the CSV file.
*
* @param string $path
* @param string $data_source
* @param string $separator
* @param string $id_prefix
* @param string $name_prefix
*
* @return array
*/
public static function loadCSV($input, $data_source = 'custom', $separator = ',', $id_prefix = '', $name_prefix = '')
{
if (!$separator)
{
return [];
}
if ($data_source === 'csv_file')
{
if (!file_exists($input))
{
return [];
}
if (!$input = file_get_contents($input))
{
return [];
}
}
if (!$data = self::getData($input, $separator, $id_prefix, $name_prefix))
{
return [];
}
return $data;
}
/**
* Iterates over the given data and returns the inputs and choices.
*
* @param string $data
* @param string $separator
* @param string $id_prefix
* @param string $name_prefix
*
* @return array
*/
public static function getData($data = '', $separator = ',', $id_prefix = '', $name_prefix = '')
{
if (!$data || !is_string($data))
{
return;
}
if (!$rows = explode(PHP_EOL, $data))
{
return;
}
$choices = [];
$inputs = [];
foreach ($rows as $row)
{
$row = explode($separator, $row);
$row = array_filter($row, 'strlen');
// if an empty row was found, skip it
if (empty($row))
{
continue;
}
if (empty($inputs))
{
$i = 1;
foreach ($row as $index => $item)
{
if ($i % 10 == 0)
{
$i++;
}
$inputs[] = [
'id' => $id_prefix . $i,
'name' => $name_prefix . '[' . $i . ']',
'label' => trim($item),
];
$i++;
}
continue;
}
$parent = null;
foreach($row as $item)
{
$item = trim($item);
if ($parent === null)
{
$parent = &$choices;
}
if (!isset($parent[$item]))
{
$item = trim($item);
$parent[$item] = [
'text' => $item,
'value' => $item,
'isSelected' => false,
'choices' => []
];
}
$parent = &$parent[$item]['choices'];
}
}
self::array_values_recursive($choices);
if (!isset($inputs) || !isset($choices))
{
return;
}
return compact('inputs', 'choices');
}
/**
* Transforms an array to using as key an index value instead of a alphanumeric.
*
* @param array $choices
* @param string $property
*
* @return array
*/
public static function array_values_recursive(&$choices, $property = 'choices')
{
$choices = array_values($choices);
for($i = 0; $i <= count($choices); $i++)
{
if(empty($choices[$i][$property]))
{
continue;
}
$choices[$i][$property] = self::array_values_recursive($choices[$i][$property], $property);
}
return $choices;
}
} Helpers/Module.php 0000644 00000002055 15235314576 0010121 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2023 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class Module
{
/**
* Get a module data.
*
* @param integer $value
* @param string $selector
*
* @return object
*/
public static function getData($value, $selector = 'id')
{
if (!$value)
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query
->select($db->quoteName(['params']))
->from($db->quoteName('#__modules'))
->where($db->quoteName($selector) . ' = ' . $db->quote($value))
->where($db->quoteName('access') . ' = 1');
$db->setQuery($query);
if (!$result = $db->loadResult())
{
return;
}
return new \Joomla\Registry\Registry($result);
}
} Helpers/Geo.php 0000644 00000001516 15235314576 0007407 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
class Geo
{
/**
* Detect and return the visitor's country.
*
* @return string The visitor's country code (GR)
*/
public static function getVisitorCountryCode()
{
$path = JPATH_PLUGINS . '/system/tgeoip/';
if (!is_dir($path))
{
return '';
}
if (!class_exists('TGeoIP'))
{
@include_once $path . 'vendor/autoload.php';
@include_once $path . 'helper/tgeoip.php';
}
$geo = new \TGeoIP();
return $geo->getCountryCode();
}
} Helpers/License.php 0000644 00000001614 15235314576 0010256 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use Joomla\CMS\Http\HttpFactory;
class License
{
/**
* Returns the remote license data from the server for the given download key.
*
* @return array
*/
public static function getRemoteLicenseData($download_key = null)
{
if (!$download_key)
{
return;
}
// License Check Endpoint
$url = TF_CHECK_LICENSE;
// Set Download Key
$url = str_replace('{{DOWNLOAD_KEY}}', $download_key, $url);
$response = HttpFactory::getHttp()->get($url);
// No response, abort
if (!$response = $response->body)
{
return;
}
return json_decode($response, true);
}
} Helpers/Number.php 0000644 00000002746 15235314576 0010133 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
class Number
{
/**
* Converts a number into a short version, eg: 1000 -> 1k
*
* @param Number $n The number to create the shorter version
* @param integer $precision
*
* @return string The shorter version of the given number
*/
public static function toShortFormat($n, $precision = 1)
{
if ($n < 900)
{
// 0 - 900
$n_format = number_format($n, $precision);
$suffix = '';
} else if ($n < 900000)
{
// 0.9k-850k
$n_format = number_format($n / 1000, $precision);
$suffix = 'K';
} else if ($n < 900000000)
{
// 0.9m-850m
$n_format = number_format($n / 1000000, $precision);
$suffix = 'M';
} else if ($n < 900000000000)
{
// 0.9b-850b
$n_format = number_format($n / 1000000000, $precision);
$suffix = 'B';
} else
{
// 0.9t+
$n_format = number_format($n / 1000000000000, $precision);
$suffix = 'T';
}
// Remove unecessary zeroes after decimal. "1.0" -> "1"; "1.00" -> "1"
// Intentionally does not affect partials, eg "1.50" -> "1.50"
if ($precision > 0)
{
$dotzero = '.' . str_repeat( '0', $precision );
$n_format = str_replace( $dotzero, '', $n_format );
}
return $n_format . $suffix;
}
} Helpers/Template.php 0000644 00000002152 15235314576 0010445 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use NRFramework\Cache;
use Joomla\CMS\Factory;
class Template
{
/**
* Returns the current template name.
*
* @return string
*/
public static function getTemplateName()
{
$hash = 'TFGetTemplateName';
if (Cache::has($hash))
{
return Cache::get($hash);
}
$template = null;
if (Factory::getApplication()->isClient('site'))
{
$template = Factory::getApplication()->getTemplate();
}
else
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('template'))
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('home') . ' = 1');
$db->setQuery($query);
$template = $db->loadResult();
}
return Cache::set($hash, $template);
}
} Helpers/CustomField.php 0000644 00000002113 15235314576 0011105 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use \Joomla\Registry\Registry;
class CustomField
{
/**
* Get a custom field's data.
*
* @param integer $value
* @param string $selector
*
* @return object
*/
public static function getData($value, $selector = 'id')
{
if (!$value)
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true);
$query
->select($db->quoteName(['fieldparams']))
->from($db->quoteName('#__fields'))
->where($db->quoteName($selector) . ' = ' . $db->quote($value))
->where($db->quoteName('state') . ' = 1');
$db->setQuery($query);
if (!$result = $db->loadResult())
{
return;
}
return new Registry($result);
}
} Helpers/Controls/Spacing.php 0000644 00000005316 15235314576 0012066 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Controls;
defined('_JEXEC') or die;
use NRFramework\Helpers\Controls\Control;
class Spacing
{
/**
* Parses the given value and returns the value expected by Spacing Control.
*
* @param mixed $value
* @param string $type This can be margin_padding or border_radius.
*
* @return array
*/
public static function parseInputValue($value = '', $type = 'margin_padding')
{
if (!$value)
{
return [];
}
$linked = isset($value['linked']) ? $value['linked'] : '0';
$unit = isset($value['unit']) ? $value['unit'] : 'px';
$value = isset($value['value']) ? $value['value'] : $value;
$positions = Control::getPositions($type);
// If it's a string of values, prepare it to be an array and continue
if (is_scalar($value))
{
$value = array_filter(explode(' ', $value), function($value) {
return $value !== '';
});
// Get the unit from the first found value
foreach ($value as $val)
{
$_value = Control::findUnitInValue($val);
if (!isset($_value['unit']))
{
continue;
}
$unit = !empty($_value['unit']) ? $_value['unit'] : $unit;
break;
}
// Ensure only ints are in the array
$value = array_map('intval', $value);
// If only a single value is given, apply the value to all positions
if (count($value) === 1)
{
$value = array_merge($value, $value, $value, $value);
}
if (count($value) === 2)
{
$value = [$value[0], $value[1], $value[0], $value[1]];
}
$tmp_value = [];
foreach ($positions as $index => $pos)
{
$tmp_value[$pos] = isset($value[$index]) ? $value[$index] : '';
}
$value = $tmp_value;
}
// Return value
$return = [];
foreach ($positions as $pos)
{
$return[$pos] = isset($value[$pos]) && $value[$pos] !== '' ? intval($value[$pos]) : '';
}
if (empty($return))
{
return [];
}
$return['linked'] = $linked;
$return['unit'] = $unit;
return $return;
}
} Helpers/Controls/Control.php 0000644 00000015311 15235314576 0012116 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Controls;
defined('_JEXEC') or die;
class Control
{
/**
* Finds the value and unit in the given subject.
*
* @param string/array $subject
*
* @return array
*/
public static function findUnitInValue($subject = '')
{
if (is_null($subject))
{
return;
}
if (is_string($subject) && $subject === '')
{
return;
}
if (is_array($subject) && count($subject) === 0)
{
return;
}
if ($subject === 'auto')
{
return [
'value' => '',
'unit' => 'auto'
];
}
if (is_array($subject) && isset($subject['value']))
{
$return = [
'value' => $subject['value']
];
if (isset($subject['unit']))
{
$return['unit'] = $subject['unit'];
}
return $return;
}
$pattern = '/^([\d.]+)(\D+)?$/';
if (is_scalar($subject) && preg_match($pattern, $subject, $matches, PREG_OFFSET_CAPTURE) === 1)
{
return [
'value' => $matches[1][0],
'unit' => isset($matches[2][0]) ? $matches[2][0] : ''
];
}
return [
'value' => $subject,
'unit' => ''
];
}
/**
* Parses the given value to a CSS value.
*
* @param mixed $value
* @param string $unit
*
* @return string
*/
public static function getCSSValue($value = '', $unit = '')
{
if (is_null($value) || $value === '')
{
return;
}
// Is scalar, transform to array
if (is_scalar($value))
{
$value = array_filter(explode(' ', $value), function($value) {
return $value !== '';
});
}
if (!$value)
{
return;
}
if (is_array($value))
{
if (empty($value))
{
return;
}
// If all values are empty, abort
$isEmptyArray = array_filter($value, function($str) {
return $str === null || $str === false || $str === '' || (is_array($str) && empty($str));
});
if (count($isEmptyArray) === 4)
{
return;
}
// Apply spacing positions
if ($positions = self::findSpacingPositions($value))
{
$return = [];
foreach ($positions as $pos)
{
$return[$pos] = isset($value[$pos]) && $value[$pos] !== '' ? $value[$pos] : 0;
}
if (empty($return))
{
return;
}
$value = $return;
}
/**
* All values are duplicates, return only 1 number with their unit.
*
* Example: Given [5, 5, 5, 5] to print the margin in pixels, do not return `margin: 5px 5px 5px 5px`.
* Rather return `margin: 5px`
*/
if (count($value) === 4 && count(array_unique($value)) === 1)
{
$value = reset($value);
if ($value_data = self::findUnitInValue($value))
{
$value = $value_data['value'];
$unit = !empty($value_data['unit']) ? $value_data['unit'] : $unit;
}
if (is_array($value))
{
return;
}
return $value . ($value > 0 ? $unit : '');
}
/**
* If we were given 4 values and first/third & second/forth values are the same then return these only.
*
* Example: Given[5, 10, 5, 10] to print the margin in pixels, do not return `margin: 5px 10px 5px 10px`.
* Rather return `margin: 5px 10px`
*/
$keys = array_keys($value);
if (count($value) === 4 && $value[$keys[0]] === $value[$keys[2]] && $value[$keys[1]] === $value[$keys[3]])
{
$value1 = $value[$keys[0]];
$suffix1 = $suffix2 = $unit;
$value2 = $value[$keys[1]];
if ($value_1 = self::findUnitInValue($value1))
{
$value1 = $value_1['value'];
$suffix1 = !empty($value_1['unit']) ? $value_1['unit'] : $unit;
}
if ($value_2 = self::findUnitInValue($value2))
{
$value2 = $value_2['value'];
$suffix2 = !empty($value_2['unit']) ? $value_2['unit'] : $unit;
}
return $value1 . ($value1 > 0 ? $suffix1 : '') . ' ' . $value2 . ($value2 > 0 ? $suffix2 : '');
}
// Different values
$data = [];
foreach ($value as $key => $_value)
{
$val = $_value;
if ($value_data = self::findUnitInValue($val))
{
$val = $value_data['value'];
$unit = !empty($value_data['unit']) ? $value_data['unit'] : $unit;
}
$data[] = $val . ($val > 0 ? $unit : '');
}
return implode(' ', $data);
}
return;
}
/**
* Finds an array of positions of the given value that
* relates to margin/padding or border radius.
*
* @param array $value
*
* @return array
*/
public static function findSpacingPositions($value = [])
{
if (!is_array($value) || !count($value))
{
return;
}
$keys = array_keys($value);
// Is margin/padding
$margin_padding = self::getPositions();
if (in_array($keys[0], $margin_padding, true))
{
return $margin_padding;
}
// Is border radius
$border_radius = self::getPositions('border_radius');
if (in_array($keys[0], $border_radius, true))
{
return $border_radius;
}
return;
}
/**
* Return the position keys based on the control type.
*
* @param string $type
*
* @return array
*/
public static function getPositions($type = 'margin_padding')
{
if (!$type)
{
return [];
}
$margin_padding = [
'top',
'right',
'bottom',
'left'
];
$border_radius = [
'top_left',
'top_right',
'bottom_right',
'bottom_left'
];
return $type === 'margin_padding' ? $margin_padding : $border_radius;
}
} Helpers/Controls/CSS.php 0000644 00000002505 15235314576 0011127 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Controls;
defined('_JEXEC') or die;
class CSS
{
public static function generateCSS($styles = [])
{
if (!$styles || !is_array($styles))
{
return;
}
$css = '';
foreach ($styles as $breakpoint => $array)
{
if (!$selectors = self::groupCSSBySelectors($array))
{
continue;
}
$css_tmp = '';
// Get all the CSS for this breakpoint for all selectors
foreach ($selectors as $selector => $_styles)
{
$css_tmp .= $selector . '{' . implode('', $_styles) . '}';
}
// Then enapsulate all the breakpoint CSS in the breakpoint media query
$css_tmp = \NRFramework\Helpers\Responsive::renderResponsiveCSS([
$breakpoint => [$css_tmp]
]);
if (!$css_tmp)
{
continue;
}
$css .= $css_tmp;
}
return $css;
}
public static function groupCSSBySelectors($styles = [])
{
if (!$styles)
{
return;
}
$selectors = [];
foreach ($styles as $style)
{
$selectors[$style['selector']][] = $style['css'];
}
if (!$selectors)
{
return;
}
return $selectors;
}
} Helpers/Controls/index.php 0000644 00000000000 15235314576 0011572 0 ustar 00 Helpers/Widgets/Gallery.php 0000644 00000027555 15235314576 0011715 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Widgets;
defined('_JEXEC') or die;
use NRFramework\Mimes;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
class Gallery
{
/**
* Stores all gallery parsed directories info txt file `*.gallery_info.txt` data in format:
* GALLERY DIRECTORY => ARRAY OF `*.gallery_info.txt` file data
*
* @var array
*/
static $gallery_directories_info_file = [];
/**
* Stores all galleries info file names in format:
*
* GALLERY DIRECTORY => INFO FILE NAME
*
* @var array
*/
static $gallery_directories_info_file_names = [];
/**
* The directory information file holding all gallery item details.
*
* @var string
*/
const directory_gallery_info_file = 'gallery_info.txt';
/**
* Parses the given gallery items.
*
* @param mixed $input A string to a directory/path/URL or an array of a URL item containing its information.
* @param array $allowed_file_types The allowed file types.
*
* @return mixed
*/
public static function parseGalleryItems($input, $allowed_file_types = [])
{
if (is_string($input))
{
$fullpath_input = JPATH_ROOT . DIRECTORY_SEPARATOR . ltrim($input, DIRECTORY_SEPARATOR);
// Parse Directory
if (is_dir($fullpath_input))
{
return self::parseDirectory($fullpath_input, $allowed_file_types);
}
// Skip invalid URLs
if ($url = self::parseURL($input))
{
return [$url];
}
// Parse Image
if ($image_data = self::parseImage($fullpath_input, $allowed_file_types))
{
return [$image_data];
}
}
return [self::parseURL($input)];
}
/**
* Parse the directory by finding all of its images and their information.
*
* @param string $dir
* @param array $allowed_file_types
*
* @return mixed
*/
public static function parseDirectory($dir, $allowed_file_types = [])
{
if (!is_string($dir) || !is_dir($dir) || empty($allowed_file_types))
{
return;
}
$items = [];
// Get images
$files = array_diff(scandir($dir), ['.', '..', '.DS_Store']);
foreach ($files as $key => $filename)
{
// Skip directories
if (is_dir($dir . DIRECTORY_SEPARATOR . $filename))
{
continue;
}
$image_path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
if (!$image_data = self::parseImage($image_path, $allowed_file_types))
{
continue;
}
$items[] = $image_data;
}
return $items;
}
/**
* Parse the directory image and return its information.
*
* @param string $image_path
* @param string $allowed_file_types
*
* @return mixed
*/
public static function parseImage($image_path, $allowed_file_types = null)
{
if (!is_string($image_path))
{
return;
}
$data = [
'path' => $image_path,
'url' => self::directoryImageToURL($image_path)
];
if (!is_file($image_path))
{
return array_merge($data, [
'invalid' => true
]);
}
// Skip not allowed file types
if (!is_null($allowed_file_types) && !Mimes::check($allowed_file_types, Mimes::detectFileType($image_path)))
{
return;
}
// Check if there is a `*.gallery_info.txt` helper file and get any information about the image
$gallery_info_file_data = self::getGalleryInfoFileData(dirname($image_path));
if (!$gallery_info_file_data)
{
return $data;
}
$image_filename = pathinfo($image_path, PATHINFO_BASENAME);
// If no information from the text field about this image was found, stop
if (!isset($gallery_info_file_data[$image_filename]))
{
return $data;
}
$image_data = $gallery_info_file_data[$image_filename];
return array_merge($data, [
'caption' => isset($image_data['caption']) ? $image_data['caption'] : ''
]);
}
/**
* Parses a single URL either as a String or as an Array.
*
* @param mixed $item
*
* @return mixed
*/
public static function parseURL($item)
{
// URL is a string
if (is_string($item))
{
if (!filter_var($item, FILTER_VALIDATE_URL))
{
return;
}
return [
'url' => $item
];
}
// URL is an array
if (!is_array($item) || !count($item))
{
return;
}
// If a thumbnail URL is given but no URL, use it as the full image URL
if (isset($item['thumbnail_url']) && !isset($item['url']))
{
$item['url'] = $item['thumbnail_url'];
}
if (!isset($item['url']))
{
return;
}
if (!filter_var($item['url'], FILTER_VALIDATE_URL))
{
return;
}
return $item;
}
/**
* Loads a module by its ID.
*
* @param string $id
*
* @return string
*/
public static function loadModule($id)
{
$module = ModuleHelper::getModuleById($id);
$params = ['style' => 'none'];
return $module->id > 0 ? Factory::getDocument()->loadRenderer('module')->render($module, $params) : '';
}
/**
* Read the `*.gallery_info.txt` file for the given directory.
*
* @param string $dir
*
* @return mixed
*/
public static function getGalleryInfoFileData($dir)
{
if (isset(self::$gallery_directories_info_file[$dir]) && !empty(self::$gallery_directories_info_file[$dir]))
{
return self::$gallery_directories_info_file[$dir];
}
if (!$file = self::findGalleryInfoFile($dir))
{
return [];
}
// Read file
if (!$handle = fopen($file, 'r'))
{
return [];
}
$data = [];
$line_defaults = ['', '', ''];
// Loop each line
while (($line = fgets($handle)) !== false)
{
list($filename, $caption, $hash) = explode('|', $line) + $line_defaults;
// If no filename is given, continue
if (!$filename)
{
continue;
}
$data[$filename] = [
'filename' => $filename,
'caption' => trim($caption),
'hash' => trim($hash)
];
}
// Close file
fclose($handle);
self::$gallery_directories_info_file[$dir] = $data;
return $data;
}
/**
* Finds the source image and whether it has been edited.
*
* @param string $source
* @param string $destination_folder
*
* @return mixed
*/
public static function findSourceImageDetails($source, $destination_folder)
{
$source_filename = pathinfo($source, PATHINFO_BASENAME);
$data = self::getGalleryInfoFileData(dirname($source));
$image_data = isset($data[$source_filename]) ? $data[$source_filename] : false;
if (!$image_data)
{
return false;
}
if (empty($image_data['hash']))
{
return false;
}
$sourceHash = self::calculateFileHash($source);
return [
'path' => $destination_folder . $image_data['filename'],
'edited' => $image_data['hash'] !== $sourceHash
];
}
/**
* Updates or Inserts the given image information from the gallery info file.
*
* @param string $source
* @param array $image_data
*
* @return mixed
*/
public static function updateImageDataInGalleryInfoFile($source, $image_data)
{
// Source directory
$source_directory = dirname($source);
// Check whether the gallery info file exists, if not, create it
if (!$file = self::findGalleryInfoFile($source_directory))
{
$file = self::createGalleryInfoFile($source_directory);
}
// Open files
$reading = fopen($file, 'r');
$writing = fopen($file . '.tmp', 'w');
$replaced = false;
while (!feof($reading))
{
// Get each file line
$line = fgets($reading);
// Remove new line at the end
$line = trim(preg_replace('/\s\s+/', ' ', $line));
// Skip empty lines
if (empty($line))
{
continue;
}
list($filename, $caption, $hash) = explode('|', $line) + ['', '', ''];
// We need to manipulate current file
if (strtolower($filename) !== strtolower(basename($image_data['path'])))
{
fputs($writing, $line . "\n");
continue;
}
$replaced = true;
$line = $filename . '|' . $caption . '|' . self::calculateFileHash($source) . "\n";;
// Write changed line
fputs($writing, $line);
}
// Close files
fclose($reading);
fclose($writing);
// If we replaced a line, update the text file
if ($replaced)
{
rename($file . '.tmp', $file);
}
// No line was replaced, append image details
else
{
unlink($file . '.tmp');
self::appendImageDataToGalleryInfoFile($file, $source, $image_data);
}
}
/**
* Removes the image from the gallery info file.
*
* @param string $source
*
* @return boolean
*/
public static function removeImageFromGalleryInfoFile($source)
{
// Get the gallery info file from destination folder
if (!$file = self::findGalleryInfoFile(dirname($source)))
{
return false;
}
// Open files
$reading = fopen($file, 'r');
$writing = fopen($file . '.tmp', 'w');
$found = false;
while (!feof($reading))
{
// Get each file line
$line = fgets($reading);
// Remove new line at the end
$line = trim(preg_replace('/\s\s+/', ' ', $line));
// Skip empty lines
if (empty($line))
{
continue;
}
list($filename, $caption, $hash) = explode('|', $line) + ['', '', ''];
// We need to manipulate current file
if ($filename !== pathinfo($source, PATHINFO_BASENAME))
{
$found = true;
fputs($writing, $line . "\n");
continue;
}
}
// Close files
fclose($reading);
fclose($writing);
if (!$found)
{
return false;
}
// Save the changes
rename($file . '.tmp', $file);
return true;
}
/**
* Appends the image data into the info file.
*
* @param string $dir
*
* @return void
*/
public static function createGalleryInfoFile($dir)
{
$file = self::getLanguageInfoFileName($dir);
file_put_contents($file, '');
return $file;
}
/**
* Appends the image data into the info file.
*
* @param string $file
* @param string $source
* @param object $image_data
*
* @return void
*/
public static function appendImageDataToGalleryInfoFile($file, $source, $image_data)
{
$caption = isset($image_data['caption']) ? $image_data['caption'] : '';
$hash = self::calculateFileHash($source);
$line = pathinfo($source, PATHINFO_BASENAME) . '|' . $caption . '|' . $hash . "\n";
file_put_contents($file, $line, FILE_APPEND);
}
/**
* Finds the `*.gallery_info.txt` file if it exists in the given directory.
*
* @param string $dir
*
* @return mixed
*/
public static function findGalleryInfoFile($dir)
{
if (isset(self::$gallery_directories_info_file_names[$dir]))
{
return self::$gallery_directories_info_file_names[$dir];
}
// Method 1: With language prefix
$file = self::getLanguageInfoFileName($dir);
// Check if the file exists
if (file_exists($file))
{
self::$gallery_directories_info_file_names[$dir] = $file;
return $file;
}
// Method 2: Without the language prefix
$file = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . self::directory_gallery_info_file;
// Check if the file exists
if (file_exists($file))
{
self::$gallery_directories_info_file_names[$dir] = $file;
return $file;
}
return false;
}
/**
* Returns the info file with the language prefix.
*
* @param string $dir
*
* @return string
*/
public static function getLanguageInfoFileName($dir)
{
return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . Factory::getLanguage()->getTag() . '.' . self::directory_gallery_info_file;
}
/**
* Calculates the file hash of a file.
*
* Hash = md5(file path + last modified date of file)
*
* @param string $file_path
*
* @return string
*/
public static function calculateFileHash($file_path)
{
return md5($file_path . filemtime($file_path));
}
/**
* Transforms an image path to a URL.
*
* @param string $image_path
*
* @return string
*/
public static function directoryImageToURL($image_path)
{
return rtrim(Uri::root(), DIRECTORY_SEPARATOR) . mb_substr($image_path, strlen(JPATH_BASE), null);;
}
} Helpers/Widgets/GalleryManager2.php 0000644 00000077532 15235314576 0013272 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Widgets;
defined('_JEXEC') or die;
use NRFramework\File;
use NRFramework\Image;
use NRFramework\Functions;
use Joomla\Registry\Registry;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Factory;
use Joomla\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File as JoomlaCMSFile;
class GalleryManager2
{
/**
* How long the files can stay in the temp folder.
*
* After each save a clean up is run and all files older
* than this value in days are removed.
*
* @var int
*/
private static $temp_files_cleanup_days = 1;
/**
* Uploads the source image to the temp folder.
*
* @param array $file The request file as posted by form
* @param string $upload_settings The upload settings
*
* @return array|bool The uploaded image paths or false on failure
*/
public static function upload($file, $uploadSettings)
{
$fullTempFolder = self::getFullTempFolder($uploadSettings['context'], $uploadSettings['field_id'], $uploadSettings['item_id']);
// Create source folder if not exists
File::createDirs($fullTempFolder);
// Move the image to the tmp folder
try {
$source = File::upload($file, $fullTempFolder, $uploadSettings['allowed_types'], $uploadSettings['allow_unsafe'], null, $uploadSettings['random_suffix']);
} catch (\Throwable $th)
{
return false;
}
if (!$source)
{
return false;
}
return str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $source);
}
public static function maybeRegenerateImages($context = 'default', $items = [], $field_id = null, $item_id = null, $oldData = [])
{
if (!$oldData)
{
return false;
}
if (!$field_data = self::getSettings($context, $field_id, $item_id))
{
return false;
}
/**
* In order to proceed, there must be changes in the following data:
*
* - provider
* - full size image
* - slideshow image
* - thumbnails
* - watermark
*/
$images = self::canRegenerateImages($field_data, $oldData);
$items = is_string($items) ? json_decode($items, true) : $items;
foreach ($items as &$item)
{
self::generateFromSource($item, $images, $field_data, false);
}
return $items;
}
/**
* Checks if the images need to be regenerated.
*
* @param object $new_data
* @param object $old_data
*
* @return array
*/
public static function canRegenerateImages($new_data = [], $old_data = [])
{
$images = [
'thumb' => false,
'slideshow' => false,
'full' => false
];
// On provider change
if ($old_data->get('provider') != $new_data->get('provider'))
{
if ($new_data->get('provider') === 'slideshow')
{
$images['slideshow'] = true;
if ($new_data->get('show_thumbnails') === '1')
{
$images['thumb'] = true;
}
}
else
{
$images['thumb'] = true;
}
}
// On thumbnails dimensions change
else if ($old_data->get('thumbnail_size') != $new_data->get('thumbnail_size') || $old_data->get('justified_item_height') != $new_data->get('justified_item_height') || $old_data->get('show_thumbnails') != $new_data->get('show_thumbnails') || $old_data->get('masonry_thumbnails_width') != $new_data->get('masonry_thumbnails_width') || $old_data->get('slideshow_thumbnail_size') != $new_data->get('slideshow_thumbnail_size'))
{
$images['thumb'] = true;
}
// Check if full image has changed
if ($old_data->get('full_image') != $new_data->get('full_image') || ($old_data->get('lightbox') != $new_data->get('lightbox') && $new_data->get('lightbox') === '1'))
{
$images['full'] = true;
}
// Check if slideshow image has changed
if ($new_data->get('provider') === 'slideshow' && $old_data->get('slideshow_image') != $new_data->get('slideshow_image'))
{
$images['slideshow'] = true;
}
// Check if watermark settings have changed
if ($old_data->get('watermark') != $new_data->get('watermark'))
{
if ($new_data->get('lightbox') === '1')
{
$images['full'] = true;
}
if ($old_data->get('slideshow_image') != $new_data->get('slideshow_image') || $new_data->get('provider') === 'slideshow')
{
$images['slideshow'] = true;
}
if ($old_data->get('provider') !== 'slideshow' || ($new_data->get('provider') === 'slideshow' && $new_data->get('show_thumbnails') === '1'))
{
$images['thumb'] = true;
}
}
return $images;
}
/**
* Returns the settings for the given context.
*
* @param string $context
* @param int $field_id
* @param int $item_id
*
* @return mixed
*/
public static function getSettings($context = 'default', $field_id = null, $item_id = null)
{
// Make sure we have a valid context
if (!$context)
{
return false;
}
$field_data = [];
if ($context === 'default')
{
// Make sure we have a valid field id
if (!$field_id)
{
return Text::_('NR_GALLERY_MANAGER_FIELD_ID_ERROR');
}
if (!$field_data = \NRFramework\Helpers\CustomField::getData($field_id))
{
return Text::_('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
}
else if ($context === 'module')
{
// Make sure we have a valid item id
if (!$item_id)
{
return Text::_('NR_GALLERY_MANAGER_ITEM_ID_ERROR');
}
if (!$field_data = \NRFramework\Helpers\Module::getData($item_id))
{
return Text::_('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
}
return $field_data;
}
/**
* Moves all given temp items over to the destination folder.
*
* @param array $value
* @param object $field
* @param string $destination_folder
*
* @return void
*/
public static function moveTempItemsToDestination($value, $field, $destination_folder)
{
if (!$destination_folder)
{
return;
}
// Create destination folder if missing
if (!File::createDirs($destination_folder))
{
return;
}
// Make field params use Registry
if (!$field->fieldparams instanceof Registry)
{
$field->fieldparams = new Registry($field->fieldparams);
}
/**
* Prepare the items for backwards compatibility
*/
$items = is_string($value) ? json_decode($value, true) ?? [['value' => $value]] : $value;
$items = isset($items['items']) ? $items['items'] : $items;
if (isset($items['value']))
{
$items = [$items];
}
$limit_files = (int) $field->fieldparams->get('limit_files', 0);
// Handle single file
if ($limit_files === 1 && is_array($items))
{
$items = [reset($items)];
}
// Compatibility Start: Migrate old items to new folder structure
self::maybeMigrateOldItems($items, $field->fieldparams);
// Compatibility End
$ds = DIRECTORY_SEPARATOR;
$images = [
'thumb' => in_array($field->fieldparams->get('provider', 'grid'), ['grid', 'masonry', 'justified']) || ($field->fieldparams->get('provider', 'grid') === 'slideshow' && $field->fieldparams->get('show_thumbnails', '0') === '1'),
'slideshow' => $field->fieldparams->get('provider', 'grid') === 'slideshow',
'full' => $field->fieldparams->get('lightbox', '0') === '1'
];
$tmpdir = Factory::getConfig()->get('tmp_path');
$tmpRelativeDirectory = ltrim(str_replace(JPATH_ROOT, '', $tmpdir), $ds);
// Move all files from the temp folder over to the `upload folder`
foreach ($items as $key => &$item)
{
/**
* Skip invalid files.
*
* These "files" can appear when we try to move files
* over to the destination folder when the gallery manager
* is still working to upload queueed files.
*/
if ($key === 'ITEM_ID')
{
continue;
}
// Skip if source does not start with the temp relative directory
$testSourcePath = ltrim(rtrim($item['source'], $ds), $ds) . $ds;
if (!Functions::startsWith($testSourcePath, $tmpRelativeDirectory . $ds))
{
continue;
}
// Move source image to final directory
try {
$source_clean = pathinfo($item['source'], PATHINFO_BASENAME);
$source_path = implode($ds, [JPATH_ROOT, $item['source']]);
$new_source_path = implode($ds, [rtrim($destination_folder, $ds), md5('source'), $source_clean]);
$new_source_path = File::move($source_path, $new_source_path);
$item['source'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $new_source_path), $ds), $ds);
} catch (\Throwable $th) {}
// Generate the rest of images from the source image
self::generateFromSource($item, $images, $field->fieldparams);
}
return $items;
}
/**
* Check and migrate old items to the new folder structure.
*
* @param array $items
* @param object $field_data
*
* @return void
*/
public static function maybeMigrateOldItems(&$items = [], $field_data = [])
{
if (!is_array($items) || empty($items))
{
return;
}
$ds = DIRECTORY_SEPARATOR;
$tmpdir = Factory::getConfig()->get('tmp_path');
// Migrate images to new folders and create source images from the full image if source is missing
foreach ($items as &$value)
{
// We skip this process if the "source" directory exists in the uploaded files path.
// This means that the new structure is present, so don't do anything.
if (!empty($value['source']))
{
$fullSourcePath = implode($ds, [JPATH_ROOT, $value['source']]);
$checkDirectory = dirname($fullSourcePath);
if (is_dir($checkDirectory) && (Functions::startsWith($checkDirectory, $tmpdir) || Functions::endsWith($checkDirectory, '/' . md5('source'))))
{
continue;
}
}
$sourceNewPath = null;
$fullImageCurrentPath = implode($ds, [JPATH_ROOT, $value['image']]);
$fullImageData = pathinfo($fullImageCurrentPath);
// Create source if missing
if (empty($value['source']))
{
$sourceNewPath = implode($ds, [dirname($fullImageCurrentPath), md5('source'), $fullImageData['basename']]);
$sourceNewPath = File::copy($fullImageCurrentPath, $sourceNewPath);
$value['source'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $sourceNewPath), $ds), $ds);
}
else
{
// Move source image
$sourceOldPath = implode($ds, [JPATH_ROOT, $value['source']]);
$sourceNewPath = implode($ds, [JPATH_ROOT, dirname($value['source']), md5('source'), $fullImageData['basename']]);
$sourceNewPath = File::move($sourceOldPath, $sourceNewPath);
$value['source'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $sourceNewPath), $ds), $ds);
}
// Delete thumbnail and recreate from source
$needThumb = false;
$thumbOldPath = implode($ds, [JPATH_ROOT, $value['thumbnail']]);
if (is_file($thumbOldPath))
{
$needThumb = true;
JoomlaCMSFile::delete($thumbOldPath);
$value['thumbnail'] = '';
if ($field_data->get('provider', 'grid') === 'slideshow' && $field_data->get('show_thumbnails', '0') === '0')
{
$needThumb = false;
}
}
// Delete full image and recreate from source
$needFull = false;
$fullImageOldPath = implode($ds, [JPATH_ROOT, $value['image']]);
if (is_file($fullImageOldPath))
{
JoomlaCMSFile::delete($fullImageOldPath);
$value['image'] = '';
if ($field_data->get('lightbox', '0') === '1')
{
$needFull = true;
}
}
self::generateFromSource($value, [
'thumb' => $needThumb,
'slideshow' => $field_data->get('provider', 'grid') === 'slideshow',
'full' => $needFull
], $field_data);
}
}
/**
* Clean up unneeded images.
*
* @param array $item
* @param object $field_data
*/
public static function cleanUpImages(&$item = [], $field_data = [])
{
$ds = DIRECTORY_SEPARATOR;
$provider = $field_data->get('provider', 'grid');
/**
* Clean up of unneeded images.
*/
/**
* Delete thumbs if:
* 1) We're using the slideshow provider and show_thumbnails is disabled and the item has a thumbnail
* 2) We're not using the slideshow provider and the item has a thumbnail and a slideshow image
*/
if (($provider === 'slideshow' && $field_data->get('show_thumbnails', '0') === '0' && $item['thumbnail']) || ($provider !== 'slideshow' && $item['thumbnail'] && $item['slideshow']))
{
$thumbnails_path = implode($ds, [JPATH_ROOT, $item['thumbnail']]);
if (is_file($thumbnails_path))
{
JoomlaCMSFile::delete($thumbnails_path);
}
$item['thumbnail'] = null;
}
// Delete full image if lightbox is disabled and the item has a full image
if ($field_data->get('lightbox') === '0' && $item['image'])
{
$full_image_path = implode($ds, [JPATH_ROOT, $item['image']]);
if (is_file($full_image_path))
{
JoomlaCMSFile::delete($full_image_path);
}
$item['image'] = null;
}
// Delete slideshow image if exists and not in slideshow provider
if ($provider !== 'slideshow' && $item['slideshow'])
{
$slideshow_image_path = implode($ds, [JPATH_ROOT, $item['slideshow']]);
if (is_file($slideshow_image_path))
{
JoomlaCMSFile::delete($slideshow_image_path);
}
$item['slideshow'] = null;
}
}
/**
* This method generates the full image, thumbnail, and slideshow images from the sources.
*
* @param array $item
* @param array $images
* @param object $field_data
* @param bool $unique_filename
*
* @return void
*/
public static function generateFromSource(&$item, $images = [], $field_data = [], $unique_filename = true)
{
$ds = DIRECTORY_SEPARATOR;
$provider = $field_data->get('provider', 'grid');
/**
* Clean up of unneeded images.
*/
self::cleanUpImages($item, $field_data);
$full_source_path = implode($ds, [JPATH_ROOT, $item['source']]);
// Create full image
if ($images['full'])
{
$full_image_path = implode($ds, [JPATH_ROOT, dirname(dirname($item['source'])), 'full', basename($item['source'])]);
$full_image_resizing_dimensions = self::getImageResizingDimensions('full_image', $field_data);
if (!empty(array_filter($full_image_resizing_dimensions)))
{
// Create the full image directory if not exists
File::createDirs(dirname($full_image_path));
// Resize the full image
$full_image_path = Image::resizeByWidthOrHeight($full_source_path, $full_image_resizing_dimensions['width'], $full_image_resizing_dimensions['height'], 70, $full_image_path, 'crop', $unique_filename);
$item['image'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $full_image_path), $ds), $ds);
}
else if ($field_data->get('full_image.by', '') === 'disabled')
{
// Create the full image directory if not exists
File::createDirs(dirname($full_image_path));
// Copy image to full folder
$full_image_path = File::copy($full_source_path, $full_image_path, !$unique_filename);
$item['image'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $full_image_path), $ds), $ds);
}
}
// Create thumbnail image
if ($images['thumb'])
{
$thumbs_folder_name = $provider === 'slideshow' ? 'thumb' : '';
$thumbnail_image_path = implode($ds, array_filter([JPATH_ROOT, dirname(dirname($item['source'])), $thumbs_folder_name, basename($item['source'])]));
$thumbnail_resizing_dimensions = [
'width' => null,
'height' => null
];
if ($provider === 'justified')
{
$thumbnail_resizing_dimensions['height'] = $field_data->get('justified_item_height', 200);
}
else if ($provider === 'masonry')
{
$thumbnail_resizing_dimensions['width'] = $field_data->get('masonry_thumbnails_width', 200);
}
else
{
$thumbnail_size = $provider === 'slideshow' ? $field_data->get('slideshow_thumbnail_size', 200) : $field_data->get('thumbnail_size', 200);
$thumbnail_resizing_dimensions = [
'width' => $thumbnail_size,
'height' => $thumbnail_size
];
// If slideshow, require show_thumbnails to be enabled
if ($provider === 'slideshow' && $field_data->get('show_thumbnails', '0') === '0')
{
$thumbnail_resizing_dimensions = null;
}
}
if ($thumbnail_resizing_dimensions)
{
// Create the full image directory if not exists
File::createDirs(dirname($thumbnail_image_path));
// Resize the full image
$thumbnail_image_path = Image::resizeByWidthOrHeight($full_source_path, $thumbnail_resizing_dimensions['width'], $thumbnail_resizing_dimensions['height'], 70, $thumbnail_image_path, 'crop', $unique_filename);
$item['thumbnail'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $thumbnail_image_path), $ds), $ds);
}
}
// Create slideshow image
if ($images['slideshow'] && $provider === 'slideshow')
{
$slideshow_image_path = implode($ds, [JPATH_ROOT, dirname(dirname($item['source'])), basename($item['source'])]);
if ($slideshow_resizing_dimensions = self::getImageResizingDimensions('slideshow_image', $field_data))
{
// Create the full image directory if not exists
File::createDirs(dirname($slideshow_image_path));
// Resize the full image
$slideshow_image_path = Image::resizeByWidthOrHeight($full_source_path, $slideshow_resizing_dimensions['width'], $slideshow_resizing_dimensions['height'], 70, $slideshow_image_path, 'crop', $unique_filename);
$item['slideshow'] = ltrim(rtrim(str_replace(JPATH_ROOT, '', $slideshow_image_path), $ds), $ds);
}
}
self::applyWatermarkOnImages($item, $images, $field_data);
}
/**
* Applies the watermark on the images.
*
* @param array $payload
* @param array $images
* @param object $field_data
*
* @return void
*/
public static function applyWatermarkOnImages($payload = [], $images = [], $field_data = [])
{
if ($field_data->get('watermark.type', 'disabled') === 'disabled')
{
return;
}
$ds = DIRECTORY_SEPARATOR;
$watermarkSettings = (array) $field_data->get('watermark', []);
$apply_on_thumbnails = $watermarkSettings['apply_on_thumbnails'] === '1';
$watermarkSettings = array_merge($watermarkSettings, [
'image' => !empty($watermarkSettings['image']) ? explode('#', JPATH_SITE . DIRECTORY_SEPARATOR . $watermarkSettings['image'])[0] : null,
]);
if ($images['full'])
{
// Add watermark to full image
$watermarkPayload = array_merge($watermarkSettings, [
'source' => implode($ds, [JPATH_ROOT, $payload['image']])
]);
\NRFramework\Image::applyWatermark($watermarkPayload);
}
if ($images['slideshow'])
{
// Add watermark to slideshow image
if ($field_data->get('provider', 'grid') === 'slideshow')
{
$watermarkPayload = array_merge($watermarkSettings, [
'source' => implode($ds, [JPATH_ROOT, $payload['slideshow']])
]);
\NRFramework\Image::applyWatermark($watermarkPayload);
}
}
if ($apply_on_thumbnails && $images['thumb'])
{
// Add watermark to thumbnail
$watermarkPayload = array_merge($watermarkSettings, [
'source' => implode($ds, [JPATH_ROOT, $payload['thumbnail']])
]);
\NRFramework\Image::applyWatermark($watermarkPayload);
}
}
public static function getImageResizingDimensions($key, $field_data)
{
$width = $height = null;
if (in_array($field_data->get($key . '.by', ''), ['width', 'custom']))
{
$width = $field_data->get($key . '.width', null);
}
if (in_array($field_data->get($key . '.by', ''), ['height', 'custom']))
{
$height = $field_data->get($key . '.height', null);
}
$data = array_filter([
'width' => $width,
'height' => $height
]);
if (!isset($data['width']))
{
$data['width'] = null;
}
if (!isset($data['height']))
{
$data['height'] = null;
}
return $data;
}
/**
* Saves the tags for each item.
*
* @param array $value
*
* @return array
*/
public static function saveItemTags($value = [])
{
if (!is_array($value))
{
return $value;
}
foreach ($value as &$item)
{
if (!isset($item['tags']) || !is_string($item['tags']))
{
$item['tags'] = [];
continue;
}
if (!$itemTags = json_decode($item['tags'], true))
{
$item['tags'] = [];
continue;
}
if (!is_array($itemTags))
{
$item['tags'] = [];
continue;
}
if (!$itemTags)
{
$item['tags'] = [];
continue;
}
// Make $itemTags an array of strings
$itemTags = array_map(function($tag) {
return (string) $tag;
}, $itemTags);
/**
* Creates the new tags in the #__tags table.
*
* This returns an array of the new tag ids. If a tag isn't new (doesn't have #new# prefix), it will return 0 as its id.
*
* We will now store the IDs returned as the tags for the item.
*/
$item['tags'] = self::createTagsFromField($itemTags);
}
return $value;
}
/**
* Create any new tags by looking for #new# in the strings
*
* @param array $tags Tags text array from the field
*
* @return mixed If successful, metadata with new tag titles replaced by tag ids. Otherwise false.
*
* @since 3.1
*/
public static function createTagsFromField($tags)
{
if (empty($tags) || $tags[0] == '')
{
return;
}
// We will use the tags table to store them
if (defined('nrJ4'))
{
$tagTable = Factory::getApplication()->bootComponent('com_tags')->getMVCFactory()->createTable('Tag', 'Administrator');
}
else
{
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
$tagTable = Table::getInstance('Tag', 'TagsTable');
}
$newTags = [];
foreach ($tags as $key => $tag)
{
// Remove the #new# prefix that identifies new tags
$tagText = str_replace('#new#', '', $tag);
if ($tagText === $tag)
{
$newTags[] = (int) $tag;
}
else
{
// Clear old data if exist
$tagTable->reset();
// Try to load the selected tag
if ($tagTable->load(['title' => $tagText]))
{
$newTags[] = (int) $tagTable->id;
}
else
{
// Prepare tag data
$tagTable->id = 0;
$tagTable->title = $tagText;
$tagTable->published = 1;
$tagTable->description = '';
$tagTable->language = '*';
$tagTable->access = 1;
// Make this item a child of the root tag
$tagTable->setLocation($tagTable->getRootId(), 'last-child');
// Try to store tag
if ($tagTable->check())
{
// Assign the alias as path (autogenerated tags have always level 1)
$tagTable->path = $tagTable->alias;
if ($tagTable->store())
{
$newTags[] = (int) $tagTable->id;
}
}
}
}
}
// At this point $newTags is an array of all tag ids
return $newTags;
}
/**
* Sets the custom field item id > field id value "source" to given source image path for the original image path
*/
public static function setItemFieldSource($item_id, $field_id, $sourceImagePath, $originalImagePath)
{
// Get "value" column from #__fields_values where item_id = $item_id and $field_id = $field_id
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('value'))
->from($db->qn('#__fields_values'))
->where($db->qn('item_id') . ' = ' . $db->q($item_id))
->where($db->qn('field_id') . ' = ' . $db->q($field_id));
$db->setQuery($query);
$value = $db->loadResult();
// If value is empty, return
if (!$value)
{
return;
}
// Decode value
$value = json_decode($value, true);
// If value is empty, return
if (!$value)
{
return;
}
// If value is not an array, return
if (!is_array($value))
{
return;
}
// If value has no items, return
if (!isset($value['items']))
{
return;
}
foreach ($value['items'] as $key => &$item)
{
if ($item['image'] !== $originalImagePath)
{
continue;
}
$item['source'] = $sourceImagePath;
}
// Update value
$query = $db->getQuery(true)
->update($db->qn('#__fields_values'))
->set($db->qn('value') . ' = ' . $db->q(json_encode($value)))
->where($db->qn('item_id') . ' = ' . $db->q($item_id))
->where($db->qn('field_id') . ' = ' . $db->q($field_id));
$db->setQuery($query);
$db->execute();
}
/**
* Media Uploader files look like: https://example.com/images/sampledata/parks/banner_cradle.png
* We remove the first part (https://example.com/images/) and keep the other part (relative path to image).
*
* @param string $filename
*
* @return string
*/
private static function getFilePathFromMediaUploaderFile($filename)
{
$filenameArray = explode('images/', $filename, 2);
unset($filenameArray[0]);
$new_filepath = join($filenameArray);
return 'images/' . $new_filepath;
}
/**
* Deletes an uploaded files: source, slideshow, original, and thumbnail.
*
* @param string $source The source image path.
* @param string $slideshow The slideshow image path.
* @param string $original The original image path.
* @param string $thumbnail The thumbnail image path.
*
* @return bool
*/
public static function deleteFile($source = null, $slideshow = null, $original = null, $thumbnail = null)
{
return [
'deleted_source_image' => self::findAndDeleteFile($source),
'deleted_slideshow_image' => self::findAndDeleteFile($slideshow),
'deleted_full_image' => self::findAndDeleteFile($original),
'deleted_thumbnail' => self::findAndDeleteFile($thumbnail)
];
}
/**
* Deletes the file.
*
* @param string $filepath
*
* @return mixed
*/
private static function findAndDeleteFile($filepath)
{
if (!$filepath)
{
return;
}
$file = Path::clean(implode(DIRECTORY_SEPARATOR, [JPATH_ROOT, $filepath]));
return is_file($file) ? JoomlaCMSFile::delete($file) : false;
}
/**
* Cleans the temp folder.
*
* Removes any image that is 1 day or older.
*
* @return void
*/
public static function clean()
{
$temp_folder = self::getFullTempFolder();
if (!is_dir($temp_folder))
{
return;
}
// Get images
$files = array_diff(scandir($temp_folder), ['.', '..', '.DS_Store', 'index.html']);
$found = [];
foreach ($files as $key => $filename)
{
$file_path = implode(DIRECTORY_SEPARATOR, [$temp_folder, $filename]);
// Skip directories
if (is_dir($file_path))
{
continue;
}
$diff_in_miliseconds = time() - filemtime($file_path);
// Skip the file if it's not old enough
if ($diff_in_miliseconds < (60 * 60 * 24 * self::$temp_files_cleanup_days))
{
continue;
}
$found[] = $file_path;
}
if (!$found)
{
return;
}
// Delete found old files
foreach ($found as $file)
{
unlink($file);
}
}
/**
* Full temp directory where images are uploaded
* prior to them being saved in the final directory.
*
* @param string $context
* @param string $field_id
* @param string $item_id
*
* @return string
*/
public static function getFullTempFolder($context = 'default', $field_id = '', $item_id = '')
{
$tmpdir = Factory::getConfig()->get('tmp_path');
$paths = [
$tmpdir,
'tassos',
\NRFramework\VisitorToken::getInstance()->get(),
$context === 'module' ? 'smilepack' : 'acf',
'gallery',
$item_id,
$field_id
];
$paths = array_filter($paths);
return implode(DIRECTORY_SEPARATOR, $paths);
}
/**
* Deletes a specific tag from every gallery item.
*
* @param int $tag_id
* @param string $context
*
* @return void
*/
public static function deleteTagFromFieldsValues($tag_id = null, $context = '')
{
if (!$tag_id)
{
return;
}
if ($context === '')
{
self::deleteTagFromCustomFieldsByTagId($tag_id);
self::deleteTagFromSubformCustomFieldsByTagId($tag_id);
}
}
/**
* Deletes a specific tag from every gallery item custom field.
*
* @param int $tag_id
*
* @return void
*/
private static function deleteTagFromCustomFieldsByTagId($tag_id = null)
{
if (!$tag_id)
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('f.id as field_id, fv.item_id as item_id, fv.value as value')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('acfgallery'));
$db->setQuery($query);
$fields = $db->loadAssocList();
if (!$fields)
{
return;
}
foreach ($fields as $field)
{
if (!$decoded_value = json_decode($field['value'], true))
{
continue;
}
if (!isset($decoded_value['items']))
{
continue;
}
$update = false;
foreach ($decoded_value['items'] as &$item)
{
if (!isset($item['tags']))
{
continue;
}
if (!is_array($item['tags']))
{
continue;
}
if (!count($item['tags']))
{
continue;
}
$item['tags'] = array_values($item['tags']);
if (($key = array_search($tag_id, $item['tags'])) !== false)
{
$update = true;
unset($item['tags'][$key]);
}
$item['tags'] = array_values($item['tags']);
}
if (!$update)
{
continue;
}
$field['value'] = json_encode($decoded_value);
// Update field value
$query->clear()
->update('#__fields_values')
->set($db->quoteName('value') . ' = ' . $db->quote($field['value']))
->where($db->quoteName('field_id') . ' = ' . $db->quote($field['field_id']))
->where($db->quoteName('item_id') . ' = ' . $db->quote($field['item_id']));
$db->setQuery($query);
$db->execute();
}
}
/**
* Deletes a specific tag from every gallery item that exists in a subform custom field.
*
* @param int $tag_id
*
* @return void
*/
private static function deleteTagFromSubformCustomFieldsByTagId($tag_id = null)
{
if (!$tag_id)
{
return;
}
if (!$tag_id)
{
return;
}
$db = Factory::getDbo();
// Get all ACF Gallery custom field IDs
$query = $db->getQuery(true)
->select('distinct f.id')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('acfgallery'));
$db->setQuery($query);
$gallery_field_ids = array_keys($db->loadAssocList('id'));
if (!$gallery_field_ids)
{
return;
}
// Get all Subform custom fields
$query->clear()
->select('f.id as field_id, fv.item_id as item_id, fv.value as value')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('subform'));
$db->setQuery($query);
$subform_fields = $db->loadAssocList();
foreach ($subform_fields as $subform_field)
{
if (!$subform_field_items = json_decode($subform_field['value'], true))
{
continue;
}
$update = false;
foreach ($subform_field_items as $row => &$row_items)
{
if (!is_array($row_items))
{
continue;
}
foreach ($row_items as $field_name => &$field_value)
{
// Get the field id
$field_id = str_replace('field', '', $field_name);
// Check if its a gallery field
if (!in_array($field_id, $gallery_field_ids))
{
continue;
}
if (!isset($field_value['items']))
{
continue;
}
foreach ($field_value['items'] as &$item)
{
if (!isset($item['tags']))
{
continue;
}
if (!is_array($item['tags']))
{
continue;
}
if (!count($item['tags']))
{
continue;
}
$item['tags'] = array_values($item['tags']);
if (($key = array_search($tag_id, $item['tags'])) !== false)
{
$update = true;
unset($item['tags'][$key]);
}
$item['tags'] = array_values($item['tags']);
}
}
}
if (!$update)
{
continue;
}
$subform_field['value'] = json_encode($subform_field_items);
// Update subform field value
$query->clear()
->update('#__fields_values')
->set($db->quoteName('value') . ' = ' . $db->quote($subform_field['value']))
->where($db->quoteName('field_id') . ' = ' . $db->quote($subform_field['field_id']))
->where($db->quoteName('item_id') . ' = ' . $db->quote($subform_field['item_id']));
$db->setQuery($query);
$db->execute();
}
}
} Helpers/Widgets/GalleryManager.php 0000644 00000060365 15235314576 0013204 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Widgets;
defined('_JEXEC') or die;
use NRFramework\File;
use NRFramework\Image;
use NRFramework\Functions;
use Joomla\Registry\Registry;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Factory;
use Joomla\Filesystem\Path;
use Joomla\Filesystem\File as JoomlaCMSFile;
class GalleryManager
{
/**
* How long the files can stay in the temp folder.
*
* After each save a clean up is run and all files older
* than this value in days are removed.
*
* @var int
*/
private static $temp_files_cleanup_days = 1;
/**
* Upload file
*
* @param array $file The request file as posted by form
* @param string $upload_settings The upload settings
* @param array $media_uploader_file_data Media uploader related file settings
* @param array $resizeSettings The resize settings
*
* @return mixed String on success, Null on failure
*/
public static function upload($file, $upload_settings, $media_uploader_file_data, $resizeSettings)
{
// The source file name
$source = '';
// Move the image to the tmp folder
try {
$source = File::upload($file, self::getFullTempFolder(), $upload_settings['allowed_types'], $upload_settings['allow_unsafe']);
} catch (\Throwable $th)
{
return false;
}
if (!$source)
{
return false;
}
$source_file_path = $source;
$ds = DIRECTORY_SEPARATOR;
// If the file came from the Media Manager file and we are copying it, fix its filename
if ($media_uploader_file_data['is_media_uploader_file'])
{
$media_uploader_file_data['media_uploader_filename'] = self::getFilePathFromMediaUploaderFile($media_uploader_file_data['media_uploader_filename']);
}
$source_image_relative = '';
$original_image_relative = '';
// Create source image by cloning the original image
$original_image_extension = pathinfo($source, PATHINFO_EXTENSION);
$original_image_destination = str_replace('.' . $original_image_extension, '_original.' . $original_image_extension, $source);
// Thumbnail file name
$thumb_image_destination = str_replace('.' . $original_image_extension, '_thumb.' . $original_image_extension, $source);
// Check whether to copy and resize the original image
if ($resizeSettings['original_image_resize'])
{
if ($resizeSettings['original_image_resize_width'] && $resizeSettings['original_image_resize_height'])
{
$original_image_full = Image::resize($source, $resizeSettings['original_image_resize_width'], $resizeSettings['original_image_resize_height'], 70, 'crop', $original_image_destination, true);
}
else if ($resizeSettings['original_image_resize_width'])
{
$original_image_full = Image::resizeAndKeepAspectRatio($source, $resizeSettings['original_image_resize_width'], 70, $original_image_destination, true);
}
else if ($resizeSettings['original_image_resize_height'])
{
$original_image_full = Image::resizeByHeight($source, $resizeSettings['original_image_resize_height'], $original_image_destination, 70, true);
}
$original_image_relative = str_replace(JPATH_ROOT . $ds, '', $original_image_full);
// Delete raw image as not needed
JoomlaCMSFile::delete($source);
}
else
{
// Original image must always be cloned by the resized original image
$original_image_full = File::move($source, $original_image_destination);
$original_image_relative = str_replace(JPATH_ROOT . $ds, '', $original_image_full);
}
// Generate thumbnails
if (!$thumb_data = self::generateThumbnail($original_image_full, $thumb_image_destination, $resizeSettings))
{
return false;
}
// Add watermark image
if (isset($upload_settings['watermark']['type']) && $upload_settings['watermark']['type'] !== 'disabled')
{
// Clone source image from original image and hash it
$source_image_full = File::copy($original_image_full, $source_file_path, false, true);
$source_image_relative = str_replace(JPATH_ROOT . $ds, '', $source_image_full);
// Add watermark to original image
$payload = array_merge($upload_settings['watermark'], [
'source' => $original_image_full
]);
\NRFramework\Image::applyWatermark($payload);
if (isset($upload_settings['watermark']['apply_on_thumbnails']) && $upload_settings['watermark']['apply_on_thumbnails'])
{
// Add watermark to original image
$payload = array_merge($upload_settings['watermark'], [
'source' => implode($ds, [self::getFullTempFolder(), $thumb_data['resized_filename']])
]);
\NRFramework\Image::applyWatermark($payload);
}
}
$tmp_folder = self::getTempFolder();
return [
'source' => $source_image_relative ? $source_image_relative : '',
'original' => $original_image_relative,
'thumbnail' => implode($ds, [$tmp_folder, $thumb_data['resized_filename']])
];
}
/**
* Moves all given `tmp` items over to the destination folder.
*
* @param array $value
* @param object $field
* @param string $destination_folder
*
* @return void
*/
public static function moveTempItemsToDestination($value, $field, $destination_folder)
{
if (!$destination_folder)
{
return;
}
// Create destination folder if missing
if (!File::createDirs($destination_folder))
{
return;
}
// Make field params use Registry
if (!$field->fieldparams instanceof Registry)
{
$field->fieldparams = new Registry($field->fieldparams);
}
/**
* Prepare the items for backwards compatibility
*/
$items = is_string($value) ? json_decode($value, true) ?? [['value' => $value]] : $value;
$items = isset($items['items']) ? $items['items'] : $items;
if (isset($items['value']))
{
$items = [$items];
}
$limit_files = (int) $field->fieldparams->get('limit_files', 0);
// Handle single file
if ($limit_files === 1 && is_array($items))
{
$items = [reset($items)];
}
$ds = DIRECTORY_SEPARATOR;
// Move all files from `tmp` folder over to the `upload folder`
foreach ($items as $key => &$item)
{
/**
* Skip invalid files.
*
* These "files" can appear when we try to move files
* over to the destination folder when the gallery manager
* is still working to upload queueed files.
*
* Also skip any items that have no value.
*/
if ($key === 'ITEM_ID' || empty($item['thumbnail']))
{
continue;
}
$moved = false;
// Ensure thumbnail in temp folder file exists
$thumbnail_clean = pathinfo($item['thumbnail'], PATHINFO_BASENAME);
$thumbnail_path = implode($ds, [JPATH_ROOT, $item['thumbnail']]);
// Move thumbnail image
if (Functions::startsWith($item['thumbnail'], self::getTempFolder()) && file_exists($thumbnail_path))
{
// Move thumbnail
$thumb = File::move($thumbnail_path, $destination_folder . $thumbnail_clean);
// Update thumbnail file name
$item['thumbnail'] = pathinfo($thumb, PATHINFO_BASENAME);
$moved = true;
}
// Check if we have uploaded the full image as well and set it
$image_clean = pathinfo($item['image'], PATHINFO_BASENAME);
$image_path = implode($ds, [JPATH_ROOT, $item['image']]);
// Move original image
if (Functions::startsWith($item['image'], self::getTempFolder()) && file_exists($image_path))
{
// Move image
$image = File::move($image_path, $destination_folder . $image_clean);
// Update image file name
$item['image'] = pathinfo($image, PATHINFO_BASENAME);
$moved = true;
}
// Ensure source in temp folder file exists
$item['source'] = isset($item['source']) ? $item['source'] : '';
// If source does not exist, create it from the original image, only if watermark is enabled
if (!$item['source'] && $field->fieldparams->get('watermark.type', 'disabled') !== 'disabled')
{
// Create source from original image
$source = File::copy($image_path, $image_path, false, true);
// Update source file name
$item['source'] = pathinfo($source, PATHINFO_BASENAME);
$moved = true;
}
// Move source image
$source_clean = pathinfo($item['source'], PATHINFO_BASENAME);
$source_path = implode($ds, [JPATH_ROOT, $item['source']]);
if (Functions::startsWith($item['source'], self::getTempFolder()) && file_exists($source_path))
{
// Move source
$thumb = File::move($source_path, $destination_folder . $source_clean);
// Update source file name
$item['source'] = pathinfo($thumb, PATHINFO_BASENAME);
$moved = true;
}
if ($moved)
{
// Update destination path
self::updateDestinationPath($item, $destination_folder);
}
}
return $items;
}
/**
* Saves the tags for each item.
*
* @param array $value
*
* @return array
*/
public static function saveItemTags($value = [])
{
if (!is_array($value))
{
return $value;
}
foreach ($value as &$item)
{
if (!isset($item['tags']) || !is_string($item['tags']))
{
$item['tags'] = [];
continue;
}
if (!$itemTags = json_decode($item['tags'], true))
{
$item['tags'] = [];
continue;
}
if (!is_array($itemTags))
{
$item['tags'] = [];
continue;
}
if (!$itemTags)
{
$item['tags'] = [];
continue;
}
// Make $itemTags an array of strings
$itemTags = array_map(function($tag) {
return (string) $tag;
}, $itemTags);
/**
* Creates the new tags in the #__tags table.
*
* This returns an array of the new tag ids. If a tag isn't new (doesn't have #new# prefix), it will return 0 as its id.
*
* We will now store the IDs returned as the tags for the item.
*/
$item['tags'] = self::createTagsFromField($itemTags);
}
return $value;
}
/**
* Create any new tags by looking for #new# in the strings
*
* @param array $tags Tags text array from the field
*
* @return mixed If successful, metadata with new tag titles replaced by tag ids. Otherwise false.
*
* @since 3.1
*/
public static function createTagsFromField($tags)
{
if (empty($tags) || $tags[0] == '')
{
return;
}
// We will use the tags table to store them
if (defined('nrJ4'))
{
$tagTable = Factory::getApplication()->bootComponent('com_tags')->getMVCFactory()->createTable('Tag', 'Administrator');
}
else
{
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
$tagTable = Table::getInstance('Tag', 'TagsTable');
}
$newTags = [];
foreach ($tags as $key => $tag)
{
// Remove the #new# prefix that identifies new tags
$tagText = str_replace('#new#', '', $tag);
if ($tagText === $tag)
{
$newTags[] = (int) $tag;
}
else
{
// Clear old data if exist
$tagTable->reset();
// Try to load the selected tag
if ($tagTable->load(['title' => $tagText]))
{
$newTags[] = (int) $tagTable->id;
}
else
{
// Prepare tag data
$tagTable->id = 0;
$tagTable->title = $tagText;
$tagTable->published = 1;
$tagTable->description = '';
$tagTable->language = '*';
$tagTable->access = 1;
// Make this item a child of the root tag
$tagTable->setLocation($tagTable->getRootId(), 'last-child');
// Try to store tag
if ($tagTable->check())
{
// Assign the alias as path (autogenerated tags have always level 1)
$tagTable->path = $tagTable->alias;
if ($tagTable->store())
{
$newTags[] = (int) $tagTable->id;
}
}
}
}
}
// At this point $newTags is an array of all tag ids
return $newTags;
}
/**
* Updates the destination path for the image and its thumbnail to the final destination folder.
*
* @param array $item
* @param string $destination_folder
*
* @return mixed
*/
private static function updateDestinationPath(&$item, $destination_folder)
{
$ds = DIRECTORY_SEPARATOR;
// Ensure destination folder is a relative path
$destination_folder = ltrim(rtrim(str_replace(JPATH_ROOT, '', $destination_folder), $ds), $ds);
$item = array_merge($item, [
'source' => !empty($item['source']) && !Functions::startsWith($item['source'], $destination_folder) ? implode($ds, [$destination_folder, $item['source']]) : $item['source'],
'thumbnail' => !Functions::startsWith($item['thumbnail'], $destination_folder) ? implode($ds, [$destination_folder, $item['thumbnail']]) : $item['thumbnail'],
'image' => !Functions::startsWith($item['image'], $destination_folder) ? implode($ds, [$destination_folder, $item['image']]) : $item['image']
]);
}
/**
* Sets the custom field item id > field id value "source" to given source image path for the original image path
*/
public static function setItemFieldSource($item_id, $field_id, $sourceImagePath, $originalImagePath)
{
// Get "value" column from #__fields_values where item_id = $item_id and $field_id = $field_id
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->qn('value'))
->from($db->qn('#__fields_values'))
->where($db->qn('item_id') . ' = ' . $db->q($item_id))
->where($db->qn('field_id') . ' = ' . $db->q($field_id));
$db->setQuery($query);
$value = $db->loadResult();
// If value is empty, return
if (!$value)
{
return;
}
// Decode value
$value = json_decode($value, true);
// If value is empty, return
if (!$value)
{
return;
}
// If value is not an array, return
if (!is_array($value))
{
return;
}
// If value has no items, return
if (!isset($value['items']))
{
return;
}
// Loop all items until we find a "image" = $originalImagePath and then set 'source' = $sourceImagePath
foreach ($value['items'] as $key => &$item)
{
if ($item['image'] !== $originalImagePath)
{
continue;
}
$item['source'] = $sourceImagePath;
}
// Update value
$query = $db->getQuery(true)
->update($db->qn('#__fields_values'))
->set($db->qn('value') . ' = ' . $db->q(json_encode($value)))
->where($db->qn('item_id') . ' = ' . $db->q($item_id))
->where($db->qn('field_id') . ' = ' . $db->q($field_id));
$db->setQuery($query);
$db->execute();
}
/**
* Media Uploader files look like: https://example.com/images/sampledata/parks/banner_cradle.png
* We remove the first part (https://example.com/images/) and keep the other part (relative path to image).
*
* @param string $filename
*
* @return string
*/
private static function getFilePathFromMediaUploaderFile($filename)
{
$filenameArray = explode('images/', $filename, 2);
unset($filenameArray[0]);
$new_filepath = join($filenameArray);
return 'images/' . $new_filepath;
}
/**
* Generates thumbnail
*
* @param string $source Source image path.
* @param string $destination Destination image path.
* @param array $resizeSettings Resize Settings.
* @param string $destination_folder Destination folder.
* @param boolean $unique_filename Whether the thumbnails will have a unique filename.
*
* @return array
*/
public static function generateThumbnail($source = '', $destination = '', $resizeSettings = [], $destination_folder = null, $unique_filename = true)
{
if (!$destination)
{
$parts = pathinfo($source);
$destination_folder = !is_null($destination_folder) ? $destination_folder : $parts['dirname'] . DIRECTORY_SEPARATOR;
$destination = $destination_folder . $parts['filename'] . '_thumb.' . $parts['extension'];
}
$resized_image = null;
$thumb_width = isset($resizeSettings['thumb_width']) ? (int) $resizeSettings['thumb_width'] : null;
$thumb_height = isset($resizeSettings['thumb_height']) ? (int) $resizeSettings['thumb_height'] : null;
// If thumbnail width is null, and we have item height set, we are resizing by height
if (is_null($thumb_width) && $thumb_height && !is_null($thumb_height))
{
$resized_image = Image::resizeByHeight($source, $thumb_height, $destination, 70, $unique_filename, true, 'resize');
}
else
{
if (is_null($thumb_width) || !$thumb_width)
{
return;
}
/**
* If height is zero, then we suppose we want to keep aspect ratio.
*
* Resize with width & height: If thumbnail height is not set
* Resize and keep aspect ratio: If thumbnail height is set
*/
$resized_image = $thumb_height && !is_null($thumb_height)
?
Image::resize($source, $thumb_width, $thumb_height, 70, $resizeSettings['thumb_resize_method'], $destination, $unique_filename, true, 'resize')
:
Image::resizeAndKeepAspectRatio($source, $thumb_width, 70, $destination, $unique_filename, true, 'resize');
}
if (!$resized_image)
{
return;
}
return [
'filename' => basename($source),
'resized_filename' => basename($resized_image)
];
}
/**
* Deletes an uploaded files: source, original, and thumbnail.
*
* @param string $source The source image path.
* @param string $original The original image path.
* @param string $thumbnail The thumbnail image path.
*
* @return bool
*/
public static function deleteFile($source = null, $original = null, $thumbnail = null)
{
return [
'deleted_source_image' => self::findAndDeleteFile($source),
'deleted_original_image' => self::findAndDeleteFile($original),
'deleted_thumbnail' => self::findAndDeleteFile($thumbnail)
];
}
/**
* Deletes the file.
*
* @param string $filepath
*
* @return mixed
*/
private static function findAndDeleteFile($filepath)
{
if (!$filepath)
{
return;
}
$file = Path::clean(implode(DIRECTORY_SEPARATOR, [JPATH_ROOT, $filepath]));
return file_exists($file) ? JoomlaCMSFile::delete($file) : false;
}
/**
* Cleans the temp folder.
*
* Removes any image that is 1 day or older.
*
* @return void
*/
public static function clean()
{
$temp_folder = self::getFullTempFolder();
if (!is_dir($temp_folder))
{
return;
}
// Get images
$files = array_diff(scandir($temp_folder), ['.', '..', '.DS_Store', 'index.html']);
$found = [];
foreach ($files as $key => $filename)
{
$file_path = implode(DIRECTORY_SEPARATOR, [$temp_folder, $filename]);
// Skip directories
if (is_dir($file_path))
{
continue;
}
$diff_in_miliseconds = time() - filemtime($file_path);
// Skip the file if it's not old enough
if ($diff_in_miliseconds < (60 * 60 * 24 * self::$temp_files_cleanup_days))
{
continue;
}
$found[] = $file_path;
}
if (!$found)
{
return;
}
// Delete found old files
foreach ($found as $file)
{
unlink($file);
}
}
/**
* Full temp directory where images are uploaded
* prior to them being saved in the final directory.
*
* @return string
*/
private static function getFullTempFolder()
{
return implode(DIRECTORY_SEPARATOR, [JPATH_ROOT, self::getTempFolder()]);
}
/**
* Temp folder where images are uploaded
* prior to them being saved in the final directory.
*
* @var string
*/
public static function getTempFolder()
{
return implode(DIRECTORY_SEPARATOR, ['media', 'tfgallerymanager', 'tmp']);
}
/**
* Deletes a specific tag from every gallery item.
*
* @param int $tag_id
* @param string $context
*
* @return void
*/
public static function deleteTagFromFieldsValues($tag_id = null, $context = '')
{
if (!$tag_id)
{
return;
}
if ($context === '')
{
self::deleteTagFromCustomFieldsByTagId($tag_id);
self::deleteTagFromSubformCustomFieldsByTagId($tag_id);
}
}
/**
* Deletes a specific tag from every gallery item custom field.
*
* @param int $tag_id
*
* @return void
*/
private static function deleteTagFromCustomFieldsByTagId($tag_id = null)
{
if (!$tag_id)
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('f.id as field_id, fv.item_id as item_id, fv.value as value')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('acfgallery'));
$db->setQuery($query);
$fields = $db->loadAssocList();
if (!$fields)
{
return;
}
foreach ($fields as $field)
{
if (!$decoded_value = json_decode($field['value'], true))
{
continue;
}
if (!isset($decoded_value['items']))
{
continue;
}
$update = false;
foreach ($decoded_value['items'] as &$item)
{
if (!isset($item['tags']))
{
continue;
}
if (!is_array($item['tags']))
{
continue;
}
if (!count($item['tags']))
{
continue;
}
$item['tags'] = array_values($item['tags']);
if (($key = array_search($tag_id, $item['tags'])) !== false)
{
$update = true;
unset($item['tags'][$key]);
}
$item['tags'] = array_values($item['tags']);
}
if (!$update)
{
continue;
}
$field['value'] = json_encode($decoded_value);
// Update field value
$query->clear()
->update('#__fields_values')
->set($db->quoteName('value') . ' = ' . $db->quote($field['value']))
->where($db->quoteName('field_id') . ' = ' . $db->quote($field['field_id']))
->where($db->quoteName('item_id') . ' = ' . $db->quote($field['item_id']));
$db->setQuery($query);
$db->execute();
}
}
/**
* Deletes a specific tag from every gallery item that exists in a subform custom field.
*
* @param int $tag_id
*
* @return void
*/
private static function deleteTagFromSubformCustomFieldsByTagId($tag_id = null)
{
if (!$tag_id)
{
return;
}
if (!$tag_id)
{
return;
}
$db = Factory::getDbo();
// Get all ACF Gallery custom field IDs
$query = $db->getQuery(true)
->select('distinct f.id')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('acfgallery'));
$db->setQuery($query);
$gallery_field_ids = array_keys($db->loadAssocList('id'));
if (!$gallery_field_ids)
{
return;
}
// Get all Subform custom fields
$query->clear()
->select('f.id as field_id, fv.item_id as item_id, fv.value as value')
->from('#__fields as f')
->join('LEFT', '#__fields_values AS fv ON fv.field_id = f.id')
->where('f.type = ' . $db->quote('subform'));
$db->setQuery($query);
$subform_fields = $db->loadAssocList();
foreach ($subform_fields as $subform_field)
{
if (!$subform_field_items = json_decode($subform_field['value'], true))
{
continue;
}
$update = false;
foreach ($subform_field_items as $row => &$row_items)
{
if (!is_array($row_items))
{
continue;
}
foreach ($row_items as $field_name => &$field_value)
{
// Get the field id
$field_id = str_replace('field', '', $field_name);
// Check if its a gallery field
if (!in_array($field_id, $gallery_field_ids))
{
continue;
}
if (!isset($field_value['items']))
{
continue;
}
foreach ($field_value['items'] as &$item)
{
if (!isset($item['tags']))
{
continue;
}
if (!is_array($item['tags']))
{
continue;
}
if (!count($item['tags']))
{
continue;
}
$item['tags'] = array_values($item['tags']);
if (($key = array_search($tag_id, $item['tags'])) !== false)
{
$update = true;
unset($item['tags'][$key]);
}
$item['tags'] = array_values($item['tags']);
}
}
}
if (!$update)
{
continue;
}
$subform_field['value'] = json_encode($subform_field_items);
// Update subform field value
$query->clear()
->update('#__fields_values')
->set($db->quoteName('value') . ' = ' . $db->quote($subform_field['value']))
->where($db->quoteName('field_id') . ' = ' . $db->quote($subform_field['field_id']))
->where($db->quoteName('item_id') . ' = ' . $db->quote($subform_field['item_id']));
$db->setQuery($query);
$db->execute();
}
}
} Helpers/Widgets/MapAddress.php 0000644 00000002056 15235314576 0012326 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Helpers\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class MapAddress
{
/**
* Returns the default address details layout.
*
* @param array $address
* @param array $showAddressDetails
*
* @return string
*/
public static function getDefaultAddressDetailsLayout($address = [], $showAddressDetails = [])
{
if (empty($address) || empty($showAddressDetails))
{
return;
}
$html = '';
$template = '<div class="nrf-mapaddress-field-address-detail-item"><strong>%s</strong>: %s</div>';
foreach ($showAddressDetails as $key)
{
$value = isset($address[$key]) ? $address[$key] : '';
if (empty($value))
{
continue;
}
$html .= sprintf($template, Text::_('NR_' . strtoupper($key)), $value);
}
return $html;
}
} SmartTags/Article.php 0000644 00000012062 15235314576 0010561 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use NRFramework\Conditions\Conditions\Component\ContentBase;
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\CMS\Router\Route;
defined('_JEXEC') or die('Restricted access');
/**
* Use the {article} Smart Tags to retrieve information about a Joomla article. This Smart Tag can return the value of any property from the Joomla Article object as long as you know the name of the property. It can access details from the current browsing article and any article by providing the article's ID using the –id property.
*/
class Article extends SmartTag
{
/**
* The data object of the article loaded
*
* @var mixed
*/
protected $article;
/**
* Class constructor
*
* @param [type] $factory
* @param [type] $options
*/
public function __construct($factory = null, $options = null)
{
parent::__construct($factory, $options);
$contentAssignment = new ContentBase();
$article_id = $this->parsedOptions->get('id', null);
if (is_null($article_id) && !$contentAssignment->isSinglePage())
{
return;
}
$this->article = $contentAssignment->getItem($article_id);
}
/**
* Fetch a property from the User object
*
* @param string $key The name of the property to return
*
* @return mixed Null if property is not found, mixed if property is found
*/
public function fetchValue($key)
{
if (!$this->article)
{
return;
}
// Case SEF URL: {article.link}
if ($key == 'link')
{
if (!defined('nrJ4') && !class_exists('ContentHelperRoute'))
{
\JLoader::register('ContentHelperRoute', JPATH_ROOT . '/components/com_content/helpers/route.php');
}
$routerHelper = defined('nrJ4') ? '\Joomla\Component\Content\Site\Helper\RouteHelper' : '\ContentHelperRoute';
return Route::_($routerHelper::getArticleRoute($this->article->id, $this->article->catid, $this->article->language));
}
// Support {article.user.USER_PROPERTY} - It would be great if somehow could integrate the {user} Smart Tag here.
if (substr($key, 0, 5) == 'user.')
{
$this->article->user = $this->factory->getUser($this->article->created_by);
}
// Case custom fields: {article.field.age}
if (strpos($key, 'field.') !== false && $this->options['isPro'])
{
$fieldParts = explode('.', $key);
$fieldname = $fieldParts[1];
// Case {article.field.age.rawvalue}
$fieldProp = isset($fieldParts[2]) ? implode('.', array_slice($fieldParts, 2)) : 'value';
if ($fields = $this->fetchCustomFields())
{
return $fields->get($fieldname . '.' . $fieldProp);
}
return;
}
$articleRegistry = new Registry($this->article);
return $articleRegistry->get($key);
}
/**
* Return an assosiative array with user custoom fields
*
* @return mixed Array on success, null on failure
*/
private function fetchCustomFields()
{
if (!$this->article)
{
return;
}
$callback = function()
{
\JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php');
$prepareCustomFields = $this->parsedOptions->get('preparecustomfields', 'true') === 'true';
if (!$fields = \FieldsHelper::getFields('com_content.article', $this->article, $prepareCustomFields))
{
return;
}
$fieldsAssoc = [];
foreach ($fields as $field)
{
if ($field->type == 'subform')
{
// Make subform field values accessible via a user-friendly shortcode {article.field.[SUBFORM_FIELD_NAME].rawvalue.[ROW_INDEX].[FIELD_NAME]}
// We could just decode the rawvalue property directly but it does make use of the field IDs instead of field names which is not that user-friendly.
$rows = [];
foreach ($field->subform_rows as $row)
{
$row_ = [];
foreach ($row as $fieldName => $fieldObj)
{
$row_[$fieldName] = $fieldObj->value;
}
$rows[] = $row_;
}
$field->rawvalue = $rows;
}
$fieldsAssoc[$field->name] = $field;
}
return new Registry($fieldsAssoc);
};
return Cache::memo('fetchCustomFields' . $this->article->id, $callback);
}
} SmartTags/Date.php 0000644 00000002555 15235314576 0010061 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Date extends SmartTag
{
/**
* The date object
*
* @var object
*/
protected $date;
/**
* The timezone object
*
* @var object
*/
protected $tz;
/**
* Constructor
*
* @param object $factory The framework factory object
* @param array $options Assignment configuration options
*/
public function __construct($factory = null, $options = null)
{
parent::__construct($factory, $options);
$this->tz = new \DateTimeZone($this->factory->getApplication()->getCfg('offset', 'GMT'));
$this->date = $this->factory->getDate()->setTimezone($this->tz);
}
/**
* Returns the current date time in format Y-m-d H:i:s.
*
* For a list of all available format characters, visit: https://www.php.net/manual/en/datetime.format.php
*
* @return string
*/
public function getDate()
{
$format = $this->parsedOptions->get('format', 'Y-m-d H:i:s');
return $this->date->format($format, true);
}
} SmartTags/URL.php 0000644 00000002500 15235314576 0007634 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class URL extends SmartTag
{
/**
* Returns the complete URL of the page, including the query string. Example: https://www.site.com/blog/?category=123
*
* @return string
*/
public function getURL()
{
return $this->factory->getURI()->toString();
}
/**
* It returns the complete URL of the page, including the query string, but encoded. For instance, if the current URL is https://www.site.com/blog/?category=123 the Smart Tag will return https%3A%2F%2Fwww.site.com%2Fblog%2F%3Fcategory%3D123. This is useful when you want to pass the URL as a parameter in another URL.
*
* @return string
*/
public function getEncoded()
{
return urlencode($this->factory->getURI()->toString());
}
/**
* Returns the URL of the page without the query string. Example: https://www.site.com/blog/
*
* @return string
*/
public function getPath()
{
$url = $this->factory->getURI();
return $url::current();
}
} SmartTags/Site.php 0000644 00000001531 15235314576 0010101 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Site extends SmartTag
{
/**
* Returns the site email
*
* @return string
*/
public function getEmail()
{
return $this->app->get('mailfrom');
}
/**
* Returns the site name
*
* @return string
*/
public function getName()
{
return $this->app->get('sitename');
}
/**
* Returns the site URL
*
* @return string
*/
public function getURL()
{
$url = $this->factory->getURI();
return $url::root();
}
} SmartTags/Crawler.php 0000644 00000003470 15235314576 0010600 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use NRFramework\DOMCrawler;
defined('_JEXEC') or die('Restricted access');
/**
* Crawl DOM elements with a Smart Tag
*
* Return Text: {crawler --selector=selector [--fallback=value]}
* Return HTML: {crawler.html --selector=selector [--fallback=value]}
* Return Inner HTML: {crawler.html --selector=selector --innerhtml=true [--fallback=value] }
* Return Count: {crawler.count --selector=selector [--fallback=value]}
*
* Note: If this Smart Tag is called before the onAfterRender event and the given CSS selector represents elements in the module's output, no nodes are likely found because the module's output still needs to be rendered.
*/
class Crawler extends SmartTag
{
/**
* This is a Pro-only feature
*
* @var boolean
*/
public $proOnly = true;
public function fetchValue($key)
{
// Sanity check.
if (!$css_selector = $this->parsedOptions->get('selector'))
{
return;
}
$crawler = new DOMCrawler();
$crawler->filter($css_selector);
$fallback = $this->parsedOptions->get('fallback');
switch ($key)
{
case 'html':
return $crawler->html($fallback, $this->parsedOptions->get('innerhtml', false));
case 'attr':
return $crawler->attr($this->parsedOptions->get('attr'), $fallback);
case 'count':
return $crawler->count($fallback);
// text
default:
return $crawler->text($fallback);
}
}
} SmartTags/QueryString.php 0000644 00000002152 15235314576 0011471 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use Joomla\Registry\Registry;
use Joomla\CMS\Filter\InputFilter;
defined('_JEXEC') or die('Restricted access');
class QueryString extends SmartTag
{
/**
* Returns the value of a URL query string parameter as found in the $_GET superglobal array. For example, if the page URL is http://example.com/page.php?key1=red&key2=blue, the {querystring.key2} Smart Tag will return blue.
*
* @param string $key
*
* @return string
*/
public function fetchValue($key)
{
$query = $this->factory->getURI()->getQuery(true);
if (empty($query))
{
return;
}
// Convert array keys to lowercase
$query = array_change_key_case($query);
// Convert array to registry object so we can access any level with dot notation.
$queryReg = new Registry($query);
return InputFilter::getInstance()->clean($queryReg->get(strtolower($key)));
}
} SmartTags/Cookie.php 0000644 00000001235 15235314576 0010407 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Cookie extends SmartTag
{
/**
* This is a Pro-only feature
*
* @var boolean
*/
public $proOnly = true;
/**
* Returns the value of a cookie as stored in the visitor’s browser.
*
* @param string $key
*
* @return string
*/
public function fetchValue($key)
{
return $this->factory->getCookie($key);
}
} SmartTags/Page.php 0000644 00000005110 15235314576 0010046 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Page extends SmartTag
{
/**
* It returns the title of the page. If the page is behind a Menu Item, its Browser Page Title will be returned if not empty; otherwise, it falls back to the title of the Menu Item. If you want to display the title of a Joomla Article, use the {article.title} Smart Tag instead.
*
* @return string
*/
public function getTitle()
{
return $this->doc->getTitle();
}
/**
* It returns the page’s meta description. If the page is a Joomla Article and has a meta description set, it will be returned. Otherwise, it falls back to the menu item’s page meta description.
*
* @return string
*/
public function getDesc()
{
return $this->doc->getMetaData('description');
}
/**
* Returns the page keywords
*
* @return string
*
* @deprecated Joomla 4 stopped offering the Meta Keywords option in the Menu Item. Use {article.keywords} instead. Reference: https://github.com/joomla/joomla-cms/issues/36639
*/
public function getKeywords()
{
return $this->doc->getMetaData('keywords');
}
/**
* It returns the language code of the page. For example, if the page’s language is English or Greek, expect "en-gb" and "el-GR" as the returned value, respectively.
*
* @return string
*/
public function getLang()
{
return $this->doc->getLanguage();
}
/**
* It returns the first part of the language code of the page. For example, if the page’s language is English or Greek, expect "en" and "el" as the returned value, respectively.
*
* @return string
*/
public function getLangURL()
{
return explode('-', $this->doc->getLanguage())[0];
}
/**
* Returns the value of the generator meta tag.
*
* @return string
*/
public function getGenerator()
{
return $this->doc->getGenerator();
}
/**
* Returns the menu item’s Browser Page Title option even if it is empty.
*
* @return string
*/
public function getBrowserTitle()
{
if (!$menu = $this->app->getMenu()->getActive())
{
return '';
}
return $menu->getParams()->get('page_title');
}
} SmartTags/Day.php 0000644 00000001062 15235314576 0007711 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Day extends Date
{
/**
* Returns the numeric representation of a day of the month without leading zeros. Eg: 22.
*
* @return string
*/
public function getDay()
{
return $this->date->format('j');
}
} SmartTags/SmartTags.php 0000644 00000054605 15235314576 0011114 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\Filesystem\Folder;
use Joomla\CMS\HTML\HTMLHelper;
/**
* SmartTags replaces placeholder variables in a string
*/
class SmartTags
{
/**
* Factory Class
*
* @var object
*/
protected $factory;
/**
* Path where each extension stores
* their Smart Tags.
*
* @var array
*/
protected $paths;
/**
* Tags Array
*
* @var array
*/
protected $tags = [];
/**
* All the options that we were given.
* This is stored in case we were given options
* other then the prefix/placeholder such as a user.
* This is useful for other plugins to manipulate the user, etc...
*
* @var array
*/
protected $options;
/**
* The Smart Tags pattern used to find all available Smart Tags in a subject.
*
* @var string
*/
protected $pattern;
/**
* The Smart Tag prefix
*
* @var string
*/
protected $prefix = '';
/**
* The Smart Tag placeholder
*
* @var string
*/
private $placeholder = '{}';
/**
* Indicates whether the calculated value will be converted to text using a layout or keep the original type as returned by the value method.
* This is supposed to be set to true when the result is supposed to be used later in the code or in a API call, just like we do in Convert Forms Webhooks.
*
* @var bool
*/
private $prepareValue = true;
/**
* List of excluded files within the NRFramework\SmartTags namespace
*
* @var array
*/
protected $excluded_smart_tags_files = [
'.',
'..',
'index.php',
'SmartTag.php',
'SmartTags.php'
];
/**
* List of areas in the content that should not be parsed for Smart Tags.
*
* @var array
*/
private $protectedAreas = [];
/**
* Indicates whether the version of the extension that calls Smart Tags is Pro or Free.
*
* @var boolean
*/
private $isPro = true;
/**
* Smart Tags Constructor
*
* @param array $opts An array of options(prefix, placeholder)
* @param Factory $factory NRFramework Factory
*/
public function __construct($opts = [], $factory = null)
{
$this->options = $opts;
// set options
if (is_array($opts))
{
$this->prefix = isset($opts['prefix']) ? $opts['prefix'] : $this->prefix;
$this->placeholder = isset($opts['placeholder']) ? $opts['placeholder'] : $this->placeholder;
$this->prepareValue = isset($opts['prepareValue']) ? $opts['prepareValue'] : $this->prepareValue;
$this->isPro = isset($opts['isPro']) ? $opts['isPro'] : true;
}
$this->pattern = $this->getPattern();
// Set Factory
if (!$factory)
{
$factory = new \NRFramework\Factory();
}
$this->factory = $factory;
// register NRFramework Smart Tags
$this->register('\NRFramework\SmartTags', dirname(__DIR__) . '/SmartTags');
}
/**
* Get a cache instance of the class
*
* @param array $opts An array of options(prefix, placeholder)
* @param object $factory The framework's factory class
*
* @return object
*/
static public function getInstance($opts = [], $factory = null)
{
static $instance = null;
if ($instance === null)
{
$instance = new SmartTags($opts, $factory);
}
return $instance;
}
/**
* Registers a namespace, path and some data where Smart Tags are stored.
*
* @param string $namespace
* @param string $path
* @param array $data
*
* @return void
*/
public function register($namespace, $path, $data = [])
{
if (!$namespace || !$path)
{
return;
}
if (isset($this->paths[$namespace]))
{
return;
}
$this->paths[$namespace] = [
'path' => $path
];
if (isset($data))
{
$this->paths[$namespace]['data'] = $data;
}
}
/**
* Remove all tags starting with the given prefix.
*
* @param string $prefix The prefix
*
* @return void
*/
public function removeTagsByPrefix($prefix)
{
foreach ($this->tags as $key => $value)
{
if (substr($key, 0, strlen($prefix)) !== $prefix)
{
continue;
}
unset($this->tags[$key]);
}
return $this;
}
/**
* Adds Custom Tags to the list
*
* @param mixed $tags Tags list (Array or Object)
* @param string $prefix A string to prefix all keys
*/
public function add($tags, $prefix = null)
{
if (!$tags || !is_array($tags))
{
return;
}
// Start of Convert Forms View Submissions Compatibility Issue
// This block is added to handle the backwards compatibility issue occured in the front-end submissions view
// in Convert Forms which adds submissions smart tags with curly brackets {}.
// @deprecated - Scheduled to be removed at the end of 2021
foreach ($tags as $key => $value)
{
if (strpos($key, '{') === false)
{
continue;
}
$newKey = ltrim($key, '{');
$newKey = rtrim($newKey, '}');
$tags[$newKey] = $value;
}
// End of Convert Forms View Submissions Compatibility Issue
// Add Prefix to keys
if ($prefix)
{
foreach ($tags as $key => $value)
{
$newKey = strtolower($prefix . $key);
$tags[$newKey] = $value;
unset($tags[$key]);
}
}
$this->tags = array_merge($this->tags, $tags);
return $this;
}
/**
* Returns placeholder in 2 pieces
*
* @return array
*/
protected function getPlaceholder()
{
return str_split($this->placeholder, strlen($this->placeholder) / 2);
}
/**
* Replace tags in object recursively
*
* @param mixed $obj The data object to search for Smart Tags
*
* @return mixed
*/
public function replace($subject)
{
if (is_null($subject))
{
return $subject;
}
if (is_scalar($subject))
{
while ($matches = $this->findSmartTags($subject))
{
// This indicates whether the subject comprises solely a single shortcode or a mixture of shortcode and plain text.
$mixContent = !(count($matches) == 1 && $matches[0] == $subject);
if (!$tmpSubject = $this->replaceSmartTagsInContent($subject, $matches, $mixContent))
{
break;
}
$subject = $tmpSubject;
}
// Restore protected areas
if (!empty($this->protectedAreas))
{
foreach ($this->protectedAreas as $protectedArea)
{
$subject = str_ireplace($protectedArea[0], $protectedArea[1], $subject);
}
}
}
else
{
foreach ($subject as $key => $subject_item)
{
$value = $this->replace($subject_item);
if ($subject instanceof Registry)
{
$subject->set($key, $value);
continue;
}
if (is_object($subject))
{
$subject->$key = $value;
continue;
}
if (is_array($subject))
{
$subject[$key] = $value;
}
}
}
return $subject;
}
/**
* Finds and replaces found Smart Tags in given content
*
* @param string $content
*
* @return void
*/
private function findSmartTags(&$content)
{
if (!is_scalar($content))
{
return;
}
// Skip protected areas
$reg = '/<!-- SmartTags Skip Start -->(.*?)<!-- SmartTags Skip End -->/s';
preg_match_all($reg, $content, $protectedAreas);
if ($protectedAreas[0])
{
foreach ($protectedAreas[0] as $protectedAreaIndex => $protectedArea)
{
$hash = md5($protectedArea);
$protectedAreaWithoutComments = $protectedAreas[1][$protectedAreaIndex];
$this->protectedAreas[] = [$hash, $protectedAreaWithoutComments];
$content = str_replace($protectedArea, $hash, $content);
}
}
// if no smart tags exist in content, abort
if (!$this->textHasShortcode($content))
{
return;
}
// find all Smart Tags
preg_match_all($this->pattern, $content, $matches);
// find all Smart Tags and keep the unique only
return array_unique($matches[0]);
}
/**
* Undocumented function
*
* @param string $content
* @param array $foundSmartTags
* @param bool $mixContent Indicates whether the subject comprises solely a single shortcode or a mixture of shortcode and plain text.
* @return void
*/
private function replaceSmartTagsInContent(&$content, $foundSmartTags, $mixContent)
{
$tag_value_pairs = [];
// find values for each Smart Tag
foreach ($foundSmartTags as $tag)
{
// prepare the smart tag that is going to be processed
if (!$shortCodeObject = $this->parseShortcode($tag))
{
continue;
}
$smartTagName = $shortCodeObject['name'];
$smartTagClassName = $shortCodeObject['group'];
// Check if the tag is already processed by a previous operation or its value provided in the payload.
if (isset($this->tags[$smartTagName]))
{
$tag_value_pairs[$tag] = $this->tags[$smartTagName];
continue;
}
// OK, we don't know the value yet. Let's see if there's a method available we can call to get a value.
$smartTagNamespace = $shortCodeObject['namespace'];
// get the Smart Tag class
if (!$smartTag = $this->getSmartTagClassByName($smartTagNamespace, $smartTagClassName, $shortCodeObject['options']))
{
/**
* No method found to call. If the current Smart Tag was added via add(), remove it, otherwise, leave it as is.
*
* This is due to without this check, a Smart Tag may be given i.e. {convertforms 1} which would be removed and thus Convert Forms
* wouldn't be able to replace it. We must only remove Smart Tags that were added by add().
*/
if (count($this->tags))
{
foreach ($this->tags as $key => $value)
{
if (strpos($key, $shortCodeObject['group']) !== 0)
{
continue;
}
$tag_value_pairs[$tag] = '';
break;
}
}
continue;
}
// Set data for Smart Tag if they exist in the path data.
if (isset($this->paths[$smartTagNamespace]['data']))
{
$smartTag->setData($this->paths[$smartTagNamespace]['data']);
}
// Make sure the Smart Tag can do replacements.
if (!$smartTag->canRun())
{
continue;
}
// Get the Smart Tag value
$value = $this->getSmartTagValue($smartTag, $shortCodeObject);
// parse the value to ensure we can save it
$layout = $shortCodeObject['options'] ? $shortCodeObject['options']->get('layout', '') : null;
$this->prepareSmartTagValue($value, $layout);
// Allow modifiers to manipulate the final value.
if ($shortCodeObject['options'])
{
$modifiers = $shortCodeObject['options']->toArray();
foreach ($modifiers as $modifierKey => $modifierValue)
{
$modifierMethod = 'modifier' . $modifierKey;
if (!method_exists($this, $modifierMethod))
{
continue;
}
$this->$modifierMethod($modifierValue, $value);
}
}
// cache value
$this->tags[$smartTagName] = $value;
// replace all instances of Smart Tag with its value
$tag_value_pairs[$tag] = $value;
}
if (!$tag_value_pairs)
{
return;
}
// Replace Smart Tags found in the subject
foreach ($tag_value_pairs as $tag => $value)
{
// Convert empty objects to empty strings if necessary.
$value = empty($value) && ($this->prepareValue || $mixContent) ? '' : $value;
// In the case of scalar (int, float, string, bool) properties, make the necessary string replacements.
if (is_scalar($value))
{
$content = str_ireplace($tag, (string) $value, $content);
continue;
}
// Otherwise, do not touch the type of the variable.
$content = $value;
}
return $content;
}
/**
* Prepares the Smart Tag value prior to saving it
*
* @param string $value
*
* @return void
*/
protected function prepareSmartTagValue(&$value, $layout = '')
{
if (!$value)
{
return;
}
// string, integer, float
if (is_scalar($value))
{
if ($layout)
{
$value = str_replace('%value%', $value, $layout);
}
return;
}
// Convert objects to array
$value = (array) $value;
if ($layout)
{
foreach ($value as &$item)
{
$this->prepareSmartTagValue($item, $layout);
}
}
// Determine if we must convert the result into string
if ($this->prepareValue)
{
$implodeChar = $layout ? '' : ',';
$value = implode($implodeChar, $value);
}
}
/**
* Parse shortcode and return an array of the shortcode information like, classname, method name e.t.c.
*
* The expected shortcode syntax is as follow: {GROUP[.NAME]}
*
* The GROUP part is required and must be pointing to \NRFramework\SmartTags\GROUP file which must declare a class with the name GROUP.
* Eg: The shortcode {customer} will try to find a class with the name Customer in the \NRFramework\SmartTags\Customer namespace.
*
* The NAME part represents the name of the method in the called class.
* For example, the shortcode {customer.name} will call the getName() method in the \NRFramework\SmartTags\Customer class.
*
* If the NAME part is ommitted or is invalid, Smart Tags fallbacks to a method with the same name as the class.
* For example, the shortcode {customer} will call the getCustomer() method in the \NRFramework\SmartTags\Customer class.
*
* @param string $text
*
* @return array
*/
private function parseShortcode($text)
{
if (empty($text))
{
return;
}
// Remove placeholders and prefix from the shortcode. {device} becomes device
$placeholder = $this->getPlaceholder();
$text = ltrim($text, $placeholder[0] . $this->prefix);
$text = trim(rtrim($text, $placeholder[1]));
$shortcodeTag = $text;
$shortcodeOptions = null;
// Split shortcode into 2 parts. First part should be the Smart Tag itself and the 2nd part should be the parameters.
$firstOptionPos = strpos($text, '--');
if ($firstOptionPos !== false)
{
$shortcodeOptions = substr($text, $firstOptionPos - strlen($text));
$shortcodeTag = substr($text, 0, $firstOptionPos - 1);
}
// We expect a shortcode in 2 parts separated by a dot.
// The 1st part is the Smart Tags Group (Class Name) and the 2nd part is the Name of the actual Smart Tag (Method name, optional).
$textParts = explode('.', $shortcodeTag, 2);
$group = $textParts[0];
$key = isset($textParts[1]) ? $textParts[1] : $textParts[0];
// Find shortcode options --option=value
if (!is_null($shortcodeOptions))
{
$shortcodeOptions = $this->parseOptions($shortcodeOptions);
}
return [
'name' => $text, // Rename to shortcode
'group' => $group,
'key' => $key,
'method_name' => 'get' . $key,
'namespace' => $this->getSmartTagNamespace($group),
'options' => $shortcodeOptions
];
}
/**
* Parase shortcode options
*
* @param string $text The original short code
*
* @return mixed Null when no options are found, Registry object otherwise.
*/
public function parseOptions($text)
{
// A quick test to determine whether to proceed or not.
if (strpos($text, '--') === false)
{
return;
}
$regex = '--(.*?)[\W]';
preg_match_all('/' . $regex . '/is', $text, $params);
$options = [];
// @Todo use Regex to parse both option name and value.
for ($i = 0; $i < count($params[1]); $i++)
{
$paramName = $params[0][$i];
$thisParamPosition = mb_strpos($text, $params[0][$i]);
$nextParamPosition = isset($params[0][$i + 1]) ? mb_strpos($text, $params[0][$i + 1]) - strlen($text) : null;
$paramValue = \mb_substr($text, $thisParamPosition + strlen($paramName), $nextParamPosition);
$options[strtolower($params[1][$i])] = trim($paramValue);
}
return new Registry($options);
}
/**
* Returns the Smart Tags Value
*
* @param SmartTag $smartTag
* @param array $shortCodeObject The parsed shortcode object
*
* @return mixed
*/
protected function getSmartTagValue($smartTag, $shortCodeObject)
{
// Smart Tags method name
$smartTagMethod = $shortCodeObject['method_name'];
// make sure method exists in the Smart Tag class
if (method_exists($smartTag, $smartTagMethod))
{
return $smartTag->{$smartTagMethod}();
}
/**
* Check if the Smart Tag contains a method
* to fetch the Smart Tag we are trying to replace.
*/
if (method_exists($smartTag, 'fetchValue'))
{
return $smartTag->fetchValue($shortCodeObject['key']);
}
}
/**
* Returns the Smart Tag Class given the name of the Smart Tag
*
* @param string $smartTagNamespace
* @param string $smartTagClassName
*
* @return mixed
*/
private function getSmartTagClassByName($smartTagNamespace, $smartTagClassName, $shortcodeOptions = null)
{
// get namespace classes
$namespace_classes = $this->getNamespaceClasses($smartTagNamespace);
if (!isset($namespace_classes[strtolower($smartTagClassName)]))
{
return false;
}
$smartTagClass = $smartTagNamespace . '\\' . $namespace_classes[strtolower($smartTagClassName)];
$options = $this->options;
$options['options'] = $shortcodeOptions;
$options['isPro'] = $this->isPro;
$class = new $smartTagClass($this->factory, $options);
if (!$this->isPro && $class->proOnly)
{
return;
}
return $class;
}
/**
* Retrieves the cached namespace clases or finds them in the given path
*
* @param string $namespace
* @param string $path
*
* @return array
*/
private function getNamespaceClasses($namespace, $path = null)
{
$cache = $this->factory->getCache();
$hash = md5('nrf_smarttags_' . $namespace);
// if namespace classes are cached, retrieve them
if ($cache->has($hash))
{
return $cache->get($hash);
}
// if no cached namespace classes exist, ensure we were given a valid path
if (!$path && !is_string($path))
{
return [];
}
// find namespace classes
$namespace_classes = Folder::files($path, '.', false, false, $this->excluded_smart_tags_files);
// stores the final strtolower(class name) => actual class file name data
$classes_data = [];
// retrieve the strtolower(class name) => class file name array
foreach ($namespace_classes as $className)
{
$base_class_name = str_replace('.php', '', $className);
$classes_data[strtolower($base_class_name)] = $base_class_name;
}
// cache it
return $cache->set($hash, $classes_data);
}
/**
* Find the namespace of the class in the path list
*
* @param string $class_name
*
* @return mixed
*/
private function getSmartTagNamespace($class_name)
{
if (!$class_name && !is_string($class_name))
{
return false;
}
foreach ($this->paths as $namespace => $path_data)
{
// get namespace classes
$namespace_classes = $this->getNamespaceClasses($namespace, $path_data['path']);
if (!isset($namespace_classes[strtolower($class_name)]))
{
continue;
}
return $namespace;
}
return false;
}
/**
* Return the regular expression pattern that will be used for searches
*
* @return string
*/
private function getPattern()
{
$placeholder = $this->getPlaceholder();
$prefix = $this->prefix ? preg_quote($this->prefix) . '.' : '';
return '#(\\' . $placeholder[0] . $prefix . '([a-zA-Z]\\' . $placeholder[0] . '??[^\\' . $placeholder[0] . ']*?\\' . $placeholder[1] . '))#';
}
/**
* Super fast way to determine whether given text includes shortcodes
*
* @param string $text
*
* @return boolean
*/
private function textHasShortcode($text)
{
return StringHelper::strpos($text, $this->getPlaceholder()[0] . $this->prefix) !== false;
}
/**
* Returns list of all tags found in given paths
*
* Currently used in the Convert Forms Front-end Submissions Menu Type and in the EngageBox SmartTags modal.
*
* @deprecated since 4.5.6
*
* @return array
*/
public function get()
{
$placeholder = $this->getPlaceholder();
// get all tags that have already been added to the list
$smart_tags_data = $this->tags;
// loop all registered paths
foreach ($this->paths as $namespace => $path_data)
{
if (!isset($path_data['path']))
{
continue;
}
if (!is_dir($path_data['path']))
{
continue;
}
// find all smart tags
$files = Folder::files($path_data['path'], '.', false, false, $this->excluded_smart_tags_files);
// search all files
foreach ($files as $className)
{
$baseClassName = str_replace('.php', '', $className);
$className = $namespace . '\\' . $baseClassName;
if (!class_exists($className))
{
continue;
}
// reflection class of smart tag
$reflectionSmartTag = new \ReflectionClass($className);
// search all methods
foreach($reflectionSmartTag->getMethods() as $method)
{
// Only parse Smart Tags of current class and not from its parent
if ($method->class != ltrim($className, '\\'))
{
continue;
}
// get smart tag name from each getSmartTag method
if (strpos($method->name, 'get') !== 0)
{
continue;
}
$funcNameSplit = explode('get', $method->name);
$suffix = '';
if (strtolower($funcNameSplit[1]) != strtolower($reflectionSmartTag->getShortName()))
{
$suffix = '.' . $funcNameSplit[1];
}
$smartTagPrefix = $placeholder[0] . strtolower($reflectionSmartTag->getShortName() . $suffix) . $placeholder[1];
$smart_tags_data[$smartTagPrefix] = '';
}
}
}
return $smart_tags_data;
}
/**
* Prepares subject with Joomla Content Plugins
*
* Syntax: {shortcode --prepareContent=true}
*
* @param Mixed $modifierValue The value of the modifier provided by the user
* @param Mixed $subject The subject where we replace the Smart Tag
*
* @return void
*/
private function modifierPrepareContent($modifierValue, &$subject)
{
if ($modifierValue)
{
$subject = HTMLHelper::_('content.prepare', $subject);
}
}
/**
* Converts a number into a short version, eg: 1000 -> 1k
* Based on: https://gist.github.com/RadGH/84edff0cc81e6326029c
*
* Syntax: {shortcode --shortNumber=true}
*
* @param Mixed $modifierValue The value of the modifier provided by the user
* @param Mixed $subject The subject where we replace the Smart Tag
*
* @return void
*/
public function modifierShortNumber($modifierValue, &$subject)
{
if ($modifierValue)
{
$subject = \NRFramework\Helpers\Number::toShortFormat($subject);
}
}
/**
* Convert special characters to HTML entities
*
* Syntax: {shortcode --tmlSpecialChars=true}
*
* @param Mixed $modifierValue The value of the modifier provided by the user
* @param Mixed $subject The subject where we replace the Smart Tag
*
* @return void
*/
public function modifierHtmlSpecialChars($modifierValue, &$subject)
{
$subject = htmlspecialchars($subject);
}
} SmartTags/AcyMailing.php 0000644 00000001760 15235314576 0011216 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class AcyMailing extends SmartTag
{
/**
* This is a Pro-only feature
*
* @var boolean
*/
public $proOnly = true;
/**
* Returns the total number of subscribers of a specific list.
*
* @return mixed Null if property is not found, mixed if property is found
*/
public function getSubscribersCount()
{
if (!$list = $this->parsedOptions->get('list'))
{
return;
}
@include_once JPATH_ADMINISTRATOR . '/components/com_acym/helpers/helper.php';
if (!$acym = acym_get('class.list'))
{
return;
}
return $acym->getSubscribersCountByListId($list);
}
} SmartTags/User.php 0000644 00000015001 15235314576 0010110 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Access\Access;
defined('_JEXEC') or die('Restricted access');
/**
* Use the {user} Smart Tags to retrieve information about the currently logged-in user. This Smart Tag can return the value of any property from the Joomla User object as long as you know the property's name.
*/
class User extends SmartTag
{
protected $user;
/**
* Class constructor
*
* @param [type] $factory
* @param [type] $options
*/
public function __construct($factory = null, $options = null)
{
parent::__construct($factory, $options);
$this->user = $this->fetchUser();
}
/**
* Fetch a property from the User object
*
* @param string $key The name of the property to return
*
* @return mixed Null if property is not found, mixed if property is found
*/
public function fetchValue($key)
{
if (!$this->user)
{
return;
}
// Just in case, deny access to the 'password' property
if ($key == 'password')
{
return;
}
// Case custom fields: {user.field.age}
if (strpos($key, 'field.') !== false && $this->options['isPro'])
{
$fieldParts = explode('.', $key);
$fieldname = $fieldParts[1];
// Case {user.field.age.rawvalue}
$fieldProp = isset($fieldParts[2]) ? implode('.', array_slice($fieldParts, 2)) : 'value';
if ($fields = $this->fetchUserFields())
{
return $fields->get($fieldname . '.' . $fieldProp);
}
return;
}
// Standard user info: {user.name}
if (is_null($this->user) || $this->user->id == 0)
{
return;
}
$userRegistry = new Registry($this->user);
return $userRegistry->get($key);
}
/**
* Return an assosiative array with user custoom fields
*
* @return mixed Array on success, null on failure
*/
private function fetchUserFields()
{
$callback = function()
{
\JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php');
$prepareCustomFields = $this->parsedOptions->get('preparecustomfields', 'true') === 'true';
if (!$fields = \FieldsHelper::getFields('com_users.user', $this->user, $prepareCustomFields))
{
return;
}
$fieldsAssoc = [];
foreach ($fields as $field)
{
if ($field->type == 'subform')
{
// Make subform field values accessible via a user-friendly shortcode {user.field.[SUBFORM_FIELD_NAME].rawvalue.[ROW_INDEX].[FIELD_NAME]}
// We could just decode the rawvalue property directly but it does make use of the field IDs instead of field names which is not that user-friendly.
$rows = [];
foreach ($field->subform_rows as $row)
{
$row_ = [];
foreach ($row as $fieldName => $fieldObj)
{
$row_[$fieldName] = $fieldObj->value;
}
$rows[] = $row_;
}
$field->rawvalue = $rows;
}
$fieldsAssoc[$field->name] = $field;
}
return new Registry($fieldsAssoc);
};
return Cache::memo('fetchUserFields' . $this->user->id, $callback);
}
/**
* Return the user object
*
* @return Juser
*/
private function fetchUser()
{
return $this->factory->getUser(isset($this->options['user']) ? $this->options['user'] : null);
}
/**
* Returns the name of the user capitalized
*
* @return string
*/
public function getName()
{
if (!$name = $this->fetchValue('name'))
{
return;
}
return ucwords(strtolower($name));
}
/**
* Returns the user first name
*
* @return string
*/
public function getFirstname()
{
if (!$name = $this->getName())
{
return;
}
// Set first name
$nameParts = explode(' ', $name, 2);
$firstname = trim($nameParts[0]);
return $firstname;
}
/**
* Returns the user last name
*
* @return string
*/
public function getLastname()
{
if (!$name = $this->getName())
{
return;
}
// Set last name
$nameParts = explode(' ', $name, 2);
$lastname = isset($nameParts[1]) ? trim($nameParts[1]) : $nameParts[0];
return $lastname;
}
/**
* Returns the user login
*
* @deprecated Use {user.username}
*
* @return string
*/
public function getLogin()
{
return $this->fetchValue('username');
}
/**
* Returns the user register date
*
* @return string
*/
public function getRegisterDate()
{
if (!$date = $this->fetchValue('registerDate'))
{
return;
}
return HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC5'));
}
public function getGroups()
{
return $this->user->getAuthorisedGroups();
}
public function getGroupTitles()
{
return array_map(function($groupID)
{
return Access::getGroupTitle($groupID);
}, $this->getGroups());
}
public function getAuthLevels()
{
return $this->user->getAuthorisedViewLevels();
}
public function getAuthLevelTitles()
{
if (!$authLevels = $this->getAuthLevels())
{
return;
}
$db = $this->factory->getDbo();
$query = $db->getQuery(true)
->select($db->qn('title'))
->from('#__viewlevels')
->where($db->qn('id') . ' IN ' . '(' . implode(',', $authLevels) . ')');
$db->setQuery($query);
return $db->loadColumn();
}
} SmartTags/Time.php 0000644 00000001047 15235314576 0010075 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Time extends Date
{
/**
* Returns a 24-hour format of an hour with leading zeros. Eg: 20:30.
*
* @return string
*/
public function getTime()
{
return $this->date->format('H:i', true);
}
} SmartTags/Geo.php 0000644 00000007044 15235314576 0007714 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die();
use Joomla\CMS\Language\Text;
class Geo extends SmartTag
{
/**
* This is a Pro-only feature
*
* @var boolean
*/
public $proOnly = true;
/**
* The Geolocation object
*
* @var mixed Object on success, Null when TGeoIP plugin can't be loaded.
*/
private $geo;
/**
* Class constructor
*/
public function __construct($factory = null, $options = null)
{
parent::__construct($factory = null, $options = null);
$this->loadGeo();
}
/**
* Return the visitor's detected multilingual Country Name
*
* @return mixed String on success, null on failure
*/
public function getCountry()
{
if ($this->geo && $code = $this->geo->getCountryCode())
{
return Text::_('NR_COUNTRY_' . $code);
}
}
/**
* Return the visitor's detected Country code
*
* @return mixed String on success, null on failure
*/
public function getCountryCode()
{
if ($this->geo)
{
return $this->geo->getCountryCode();
}
}
/**
* Return the visitor's detected City name
*
* @return mixed String on success, null on failure
*/
public function getCity()
{
if ($this->geo)
{
return $this->geo->getCity();
}
}
/**
* Return the visitor's detected Regions
*
* @return mixed String on success, null on failure
*/
public function getRegion()
{
if (!$record = $this->geo)
{
return;
}
// Ensure we have regions
if (!isset($record->subdivisions))
{
return;
}
$regions = [];
// Skip if no regions found
if (!$record->subdivisions)
{
return;
}
$langCode = $this->factory->getLanguage()->getTag();
$langCode = explode('-', $langCode)[0];
foreach ($record->subdivisions as $region)
{
$regions[] = isset($region->names[$langCode]) ? $region->names[$langCode] : $region->names['en'];
}
return implode(', ', $regions);
}
/**
* Return the visitor's full geo location (Country, City, Regions)
*
* @return mixed String on success, null on failure
*/
public function getLocation()
{
$location_parts = array_filter([
$this->getCountry(),
$this->getCity(),
$this->getRegion()
]);
return implode(', ', $location_parts);
}
/**
* Load GeoIP Classes
*
* @return void
*/
private function loadGeo($ip = null)
{
if (!class_exists('TGeoIP'))
{
$path = JPATH_PLUGINS . '/system/tgeoip';
if (@file_exists($path . '/helper/tgeoip.php'))
{
if (@include_once($path . '/vendor/autoload.php'))
{
@include_once $path . '/helper/tgeoip.php';
}
}
// If for some reason the tgeoip plugin files do not exist, abort
if (!class_exists('TGeoIP'))
{
return;
}
}
$this->geo = new \TGeoIP($ip);
}
} SmartTags/RandomID.php 0000644 00000001126 15235314576 0010632 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Crypt\Crypt;
class RandomID extends SmartTag
{
/**
* Returns an 8-character hexadecimal random ID. Example: 03bc431d0d605ce4
*
* @return string
*/
public function getRandomID()
{
return bin2hex(Crypt::genRandomBytes(8));
}
} SmartTags/Post.php 0000644 00000002012 15235314576 0010115 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Post extends SmartTag
{
/**
* This is a Pro-only feature
*
* @var boolean
*/
public $proOnly = true;
/**
* Returns the value of a post data as found in the $_POST superglobal array. For example, if you submit a form that consists of the “email” and “name” input fields, you can use {post.email} and {post.name} Smart Tags in the submitted URL to retrieve the value of any form input.
*
* @param string $key
*
* @return string
*/
public function fetchValue($key)
{
$filter = $this->parsedOptions->get('filter', 'STRING');
$default_value = $this->parsedOptions->get('default', '');
return $this->app->input->post->get($key, $default_value, $filter);
}
} SmartTags/Referrer.php 0000644 00000001143 15235314576 0010750 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Referrer extends SmartTag
{
/**
* Returns the URL of the webpage where a person clicked a link that sent them to your site.
*
* @return string
*/
public function getReferrer()
{
return $this->app->input->server->get('HTTP_REFERER', '', 'RAW');
}
} SmartTags/Year.php 0000644 00000001034 15235314576 0010073 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Year extends Date
{
/**
* Returns a 4-digit numeric representation of the year. Eg: 2023.
*
* @return string
*/
public function getYear()
{
return $this->date->format('Y');
}
} SmartTags/Month.php 0000644 00000001055 15235314576 0010263 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
class Month extends Date
{
/**
* Returns the numeric representation of the month without leading zeros. Eg: 10.
*
* @return string
*/
public function getMonth()
{
return $this->date->format('n');
}
} SmartTags/Client.php 0000644 00000003164 15235314576 0010417 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
use NRFramework\WebClient;
class Client extends SmartTag
{
/**
* Returns the type of the device of the user.
*
* @return string Possible values: desktop, mobile, tablet
*/
public function getDevice()
{
return WebClient::getDeviceType();
}
/**
* Returns the operating system of the user.
*
* @return string Possible values: windows, windows phone, iphone, ipad, ipod, mac, blackberry, android, android tablet, linux
*/
public function getOS()
{
return WebClient::getOS();
}
/**
* Returns the name of the browser of the user.
*
* @return string Possible values: ie, firefox, chrome, safari, opera, edge
*/
public function getBrowser()
{
return WebClient::getBrowser()['name'];
}
/**
* Returns the user agent string of the user.
*
* @return string
*/
public function getUserAgent()
{
return WebClient::getClient()->userAgent;
}
/**
* Returns the 8-character hexadecimal ID representing the visitor's unique ID as stored in the nrid cookie in the visitor’s browser.
*
* @return string Example: 03bc431d0d605ce4
*/
public function getID()
{
return \NRFramework\VisitorToken::getInstance()->get();
}
} SmartTags/SmartTag.php 0000644 00000006226 15235314576 0010725 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
defined('_JEXEC') or die('Restricted access');
use Joomla\Registry\Registry;
abstract class SmartTag
{
/**
* Factory Class
*
* @var object
*/
protected $factory;
/**
* Joomla Application object
*
* @var object
*/
protected $app;
/**
* Joomla Document
*
* @var object
*/
protected $doc;
/**
* Useful data used by a Smart Tag
*
* @var array
*/
protected $data;
/**
* Parsed Options
*
* @var array
*/
protected $parsedOptions;
/**
* Smart Tags Configuration Options
*
* @var array
*/
protected $options;
/**
* Indicates whether this Smart Tag is a Pro-only feature
*
* @var boolean
*/
public $proOnly = false;
public function __construct($factory = null, $options = null)
{
if (!$factory)
{
$factory = new \NRFramework\Factory();
}
$this->factory = $factory;
$this->app = $this->factory->getApplication();
$this->doc = $this->factory->getDocument();
$this->parsedOptions = isset($options['options']) ? $options['options'] : new Registry();
$this->options = $options;
}
/**
* Set the data
*
* @param array $data
*
* @return void
*/
public function setData($data)
{
$this->data = $data;
}
/**
* This method runs before replacements and determines whether the class can be executed and do replacements or not.
*
* THE PROBLEM:
*
* Let's say we have a bunch of Smart Tags in a namespaced folder and we register them using the register() method.
* The Smart Tags include, Foo and Bar. Let's say our replacement subject is: 'lorem {foo.x} ipsum {foo.y} lorem ipsum {bar.x}'
* and we'd like to replace {foo.x} and {foo.y} and leave {bar.x} untouched. Right now this is not possible.
* All 3 Smart Tags will be replaced in the subject because all classes are already registered.
*
* This problem occurs also in Convert Forms during form rendering. When a form is using Calculations, it's very likely
* a calculation formula in the form {field.XXX} + {field.YYY} is included in the form's HTML layout.
* In Convert Forms, Smart Tag replacements run during page load. Since we have a Smart Tag for Fields {field.XXX} already registered,
* the Smart Tags found in the Calculations formula will be replaced by empty space (there's no submitted data yet) breaking Calculation.
*
* We need a way to determine during runtime whether a Smart Tag can run or not.
*
* We could write a new method so 3rd party extension can register individual classes conditionally but this would add more work on the extension's side.
*
* @return boolean
*/
public function canRun()
{
return true;
}
} SmartTags/Language.php 0000644 00000003235 15235314576 0010723 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use Joomla\CMS\Language\Text;
defined('_JEXEC') or die('Restricted access');
class Language extends SmartTag
{
/**
* Returns the text of a language string. Replace CONSTANT with the language constant you want to return its text. For instance, to return the text of the language string COM_CONTACT_DETAILS, use {language.COM_CONTACT_DETAILS}.
*
* @param string $key
*
* @return string
*/
public function fetchValue($key)
{
$key = strtolower($key);
$key_parts = explode('_', $key);
$lang = $this->factory->getLanguage();
// Load language overrides: On front-end load administrator's override and vice versa.
$overridePath = $this->factory->isFrontend() ? JPATH_ADMINISTRATOR : JPATH_SITE;
$lang->load($lang->getTag() . '.override', $overridePath, 'overrides');
switch ($key_parts[0])
{
case 'com':
if (isset($key_parts[1]) && !empty($key_parts[1]))
{
$extension = 'com_' . $key_parts[1];
}
$lang->load($extension, JPATH_ADMINISTRATOR);
$lang->load($extension, JPATH_SITE);
break;
case 'plg':
if (isset($key_parts[1]) && !empty($key_parts[1]) && isset($key_parts[2]) && !empty($key_parts[2]))
{
$extension = implode('_', ['plg', $key_parts[1], $key_parts[2]]);
}
$path = implode(DIRECTORY_SEPARATOR, [JPATH_PLUGINS, $key_parts[1], $key_parts[2]]);
$lang->load($extension, $path);
break;
}
return Text::_($key);
}
} SmartTags/IP.php 0000644 00000001017 15235314576 0007504 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\SmartTags;
use NRFramework\User;
defined('_JEXEC') or die('Restricted access');
class IP extends SmartTag
{
/**
* Returns the IP address of the visitor.
*
* @return string
*/
public function getIP()
{
return User::getIP();
}
} Controls/Spacing.php 0000644 00000001156 15235314576 0010462 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Controls;
defined('_JEXEC') or die;
class Spacing extends Control
{
protected function generateCSSProperty($value, $unit)
{
$value = \NRFramework\Helpers\Controls\Control::getCSSValue($value, $unit);
if ((is_null($value) || $value === '') && $value != '0')
{
return;
}
return $this->property . ':' . $value . ';';
}
} Controls/Control.php 0000644 00000022640 15235314576 0010517 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Controls;
defined('_JEXEC') or die;
class Control
{
/**
* The CSS selector related to this control.
*
* @var string|array
*/
protected $selector;
/**
* The CSS property related to this control.
*
* @var mixed
*/
protected $property;
/**
* The CSS property used when there are conditions and we fail to use the property, so we override it using this property.
*
* @var mixed
*/
protected $fallback_property;
/**
* The CSS property used when there are no conditions and we fail to use the property, so we override it using this value.
*
* @var mixed
*/
protected $fallback_value;
/**
* Some controls may render CSS conditionally, based on the given value.
*
* @var array
*/
protected $values;
/**
* The control value.
*
* @var mixed
*/
protected $value;
/**
* The raw control value.
*
* @var mixed
*/
protected $value_raw;
/**
* The control value unit.
*
* @var string
*/
protected $unit;
/**
* Exclude specific breakpoints from the control's CSS.
*
* @var array
*/
protected $exclude_breakpoints = [];
/**
* The existing controls we have parsed so far.
*
* @var array
*/
protected $parsedControls = [];
/**
* A control may require some conditions to be set.
*
* @var array
*/
protected $conditions = [];
/**
* Whether to ignore "inherit" values.
*
* @var bool
*/
protected $skip_inherit_value = false;
/**
* The current breakpoint we are checking against to set CSS.
*
* @var string
*/
protected $current_breakpoint = 'desktop';
public function __construct($payload = [])
{
$this->parsedControls = isset($payload['parsedControls']) ? $payload['parsedControls'] : null;
$this->selector = isset($payload['selector']) ? $payload['selector'] : null;
$this->conditions = isset($payload['conditions']) ? $payload['conditions'] : [];
$this->property = isset($payload['property']) ? $payload['property'] : null;
$this->skip_inherit_value = isset($payload['skip_inherit_value']) ? $payload['skip_inherit_value'] : $this->skip_inherit_value;
// The fallback_property is used when we have conditions and we don't have a value, so we override it using this property.
$this->fallback_property = isset($payload['fallback_property']) ? $payload['fallback_property'] : null;
// The fallback_value is used when we don't have conditions and we don't have a value, so we override it using this value.
$this->fallback_value = isset($payload['fallback_value']) ? $payload['fallback_value'] : null;
$this->values = isset($payload['values']) ? $payload['values'] : [];
$this->exclude_breakpoints = isset($payload['exclude_breakpoints']) ? $payload['exclude_breakpoints'] : null;
$this->value = isset($payload['value']['value']) ? $payload['value']['value'] : (isset($payload['value']) ? $payload['value'] : null);
$this->value_raw = isset($payload['value']['value']) ? $payload['value']['value'] : (isset($payload['value']) ? $payload['value'] : null);
$this->unit = isset($payload['value']['unit']) ? $payload['value']['unit'] : (isset($payload['unit']) ? $payload['unit'] : null);
if (isset($this->value['unit']))
{
unset($this->value['unit']);
}
}
public function getCSS()
{
if (!$this->isResponsive())
{
$this->value = $this->generateCSSProperty($this->value, $this->unit);
return $this->value;
}
// Prepare value for arrays
foreach ($this->value as $breakpoint => &$value)
{
$this->current_breakpoint = $breakpoint;
// If this breakpoint is excluded, skip
if (is_array($this->exclude_breakpoints) && count($this->exclude_breakpoints) && in_array($breakpoint, $this->exclude_breakpoints))
{
unset($this->value[$breakpoint]);
continue;
}
if (is_scalar($value))
{
$value = $this->generateCSSProperty($value, $this->unit);
}
else
{
$unit = isset($value['unit']) ? $value['unit'] : $this->unit;
// Remove "unit" property
if ($unit)
{
unset($value['unit']);
}
// Remove "linked" property
if (isset($value['linked']))
{
unset($value['linked']);
}
$value = isset($value['value']) ? $value['value'] : $value;
// Remove responsive value if no actual value is set
if (!$value && $value != '0' && $unit != 'auto')
{
unset($this->value[$breakpoint]);
continue;
}
$value = $this->generateCSSProperty($value, $unit);
}
if (is_null($value))
{
continue;
}
}
return $this->value;
}
protected function generateCSSProperty($value, $unit)
{
if ($this->shouldSkipPropertyGeneration($value, $unit))
{
return;
}
$conditions_pass = $this->conditionsPass($value);
$conditionsNotMet = (!$conditions_pass && !$this->fallback_property);
$emptyValueWithUnitNotAuto = ($value === '' || is_null($value)) && !$this->fallback_property && $unit !== 'auto';
if ($conditionsNotMet || $emptyValueWithUnitNotAuto)
{
return;
}
if ($this->values)
{
return $this->generateValueCSS($value);
}
$properties = $this->conditions && !$conditions_pass && $this->fallback_property ? $this->fallback_property : $this->property;
// If we have no conditions and no value, but we have a fallback value, use it.
if (!$this->conditions && !$value && $this->fallback_value)
{
$properties = $this->fallback_value;
}
if (is_array($properties))
{
return $this->generateArrayPropertyCSS($properties, $conditions_pass, $value, $unit);
}
if ($value === 'inherit' && $this->skip_inherit_value)
{
return;
}
return $this->generateSinglePropertyCSS($value, $unit);
}
private function shouldSkipPropertyGeneration($value, $unit)
{
return !$value && $value != '0' && $unit != 'auto' && !$this->fallback_property;
}
private function generateValueCSS($value)
{
$css = '';
$value = explode(' ', $value);
foreach ($this->values as $key => $css_value)
{
if (!in_array($key, $value))
{
continue;
}
$css .= implode('', $css_value);
}
return $css;
}
private function generateArrayPropertyCSS($properties, $conditions_pass, $value, $unit)
{
$css = '';
// If the conditions did pass and we do not have a value, do not set any CSS.
if ($conditions_pass && !$value && $unit !== 'auto')
{
return $css;
}
foreach ($properties as $prop_key => $prop_value)
{
$css_line = $prop_key . ':' . str_replace('%value%', $value . $unit, $prop_value) . ';';
$css_line = str_replace('%value_raw%', $value , $css_line);
$css .= $css_line;
}
return $css;
}
private function generateSinglePropertyCSS($value, $unit)
{
$value = \NRFramework\Helpers\Controls\Control::findUnitInValue($value);
$val = isset($value['value']) ? $value['value'] : $value;
$unit = isset($value['unit']) && $value['unit'] ? $value['unit'] : $unit;
return $this->getProperty() . ':' . $val . $unit . ';';
}
private function conditionsPass(&$value)
{
if (!$this->conditions)
{
return true;
}
foreach ($this->conditions as $conditionItem)
{
if (!$this->checkCondition($conditionItem, $value))
{
return false;
}
}
return true;
}
private function checkCondition($conditionItem, &$value)
{
$foundCondition = array_search($conditionItem['property'], array_column($this->parsedControls, 'property'));
if ($foundCondition === false)
{
return false;
}
$foundConditionControl = isset($this->parsedControls[$foundCondition]) ? $this->parsedControls[$foundCondition] : false;
if (!$foundConditionControl)
{
return false;
}
$conditionValue = $conditionItem['property'] . ':' . $conditionItem['value'] . ';';
$foundConditionControlValues = $foundConditionControl['control']->getValue();
$breakpointValue = is_array($foundConditionControlValues) ? ($foundConditionControlValues[$this->current_breakpoint] ?? null) : $foundConditionControlValues;
if (\NRFramework\Functions::endsWith($breakpointValue, ':inherit;'))
{
$prevBreakpoint = $this->current_breakpoint === 'tablet' ? 'desktop' : 'tablet';
$breakpointValue = is_array($foundConditionControlValues) ? ($foundConditionControlValues[$prevBreakpoint] ?? null) : $foundConditionControlValues;
// Also update the value
$value = isset($this->value_raw[$prevBreakpoint]['value']) || isset($this->value_raw[$prevBreakpoint]) ? '' : $value;
if (\NRFramework\Functions::endsWith($breakpointValue, ':inherit;') && $this->current_breakpoint === 'mobile' && $prevBreakpoint === 'tablet')
{
$breakpointValue = is_array($foundConditionControlValues) ? ($foundConditionControlValues['desktop'] ?? null) : $foundConditionControlValues;
// Also update the value
$value = isset($this->value_raw['desktop']['value']) || isset($this->value_raw['desktop']) ? '' : $value;
}
}
return $conditionValue === $breakpointValue;
}
public function getValue()
{
return $this->value;
}
public function getSelector()
{
return $this->selector;
}
public function getProperty()
{
return $this->property;
}
public function isResponsive()
{
$keys = ['desktop', 'tablet', 'mobile'];
return is_array($this->value) && !empty(array_intersect_key(array_flip($keys), $this->value));
}
} Controls/Border.php 0000644 00000001404 15235314576 0010307 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Controls;
defined('_JEXEC') or die;
class Border extends Control
{
protected function generateCSSProperty($value, $unit)
{
// We require all border attributes
if (!isset($value['width']) || !isset($value['style']) || !isset($value['color']))
{
return;
}
// Ensure the width is > 0
if (intval($value['width']) === 0)
{
return;
}
return $this->property . ':' . implode(' ', [
$value['width'] . $unit,
$value['style'],
$value['color']
]) . ';';
}
} Controls/Controls.php 0000644 00000005202 15235314576 0010675 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Controls;
defined('_JEXEC') or die;
class Controls
{
/**
* The Control Factory.
*
* @var ControlFactory
*/
protected $factory;
/**
* The main selector that will be used for all controls generated CSS.
*
* Each control can override this by setting the "selector" property.
*
* @var string
*/
protected $selector;
/**
* Define which breakpoints to exclude from the CSS generation.
*
* @var array
*/
protected $exclude_breakpoints = [];
public function __construct($factory = null, $selector = null, $exclude_breakpoints = [])
{
if (!$factory)
{
$factory = new ControlFactory();
}
$this->factory = $factory;
$this->selector = $selector;
$this->exclude_breakpoints = $exclude_breakpoints;
}
public function generateCSS($controls = [])
{
$cssArray = $this->getCSSArray($controls);
// Get the final CSS
return \NRFramework\Helpers\Controls\CSS::generateCSS($cssArray);
}
protected function getCSSArray($controls = [])
{
if (!$controls || !is_array($controls))
{
return;
}
$parsedControls = [];
$cssArray = [
'desktop' => [],
'tablet' => [],
'mobile' => []
];
// Get the responsive CSS for each control
foreach ($controls as $control_payload)
{
// Set any breakpoints to exclude when generating CSS
$control_payload['exclude_breakpoints'] = $this->exclude_breakpoints;
// Set the selector
if (!isset($control_payload['selector']))
{
$control_payload['selector'] = $this->selector;
}
$control_payload['parsedControls'] = $parsedControls;
if (!$control = $this->factory->createControl($control_payload))
{
continue;
}
if (!$control_css = $control->getCSS())
{
continue;
}
$selector = $control->getSelector();
if (isset($control_payload['property']))
{
$parsedControls[] = [
'property' => $control_payload['property'],
'control' => $control
];
}
if ($control->isResponsive())
{
foreach ($control_css as $breakpoint => $control_payload)
{
if (is_null($control_payload))
{
continue;
}
$cssArray[$breakpoint][] = [
'selector' => $selector,
'css' => $control_payload
];
}
}
else
{
$cssArray['desktop'][] = [
'selector' => $selector,
'css' => $control_css
];
}
}
return $cssArray;
}
} Controls/ControlFactory.php 0000644 00000001447 15235314576 0012051 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Controls;
defined('_JEXEC') or die;
class ControlFactory
{
public function createControl($value = [])
{
if (!isset($value['value']))
{
return;
}
$type = isset($value['type']) ? $value['type'] : 'Control';
switch ($type)
{
case 'Control':
return new \NRFramework\Controls\Control($value);
break;
case 'Border':
return new \NRFramework\Controls\Border($value);
break;
case 'Spacing':
return new \NRFramework\Controls\Spacing($value);
break;
}
return;
}
} Library/index.php 0000644 00000000000 15235314576 0007771 0 ustar 00 Library/Library.php 0000644 00000043051 15235314576 0010303 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Library;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Uri\Uri;
class Library
{
/**
* Library item info popup.
*
* @var string
*/
private $info_modal_id = 'tf-library-item-info-popup';
/**
* Library preview popup.
*
* @var string
*/
private $preview_modal_id = 'tf-library-preview-popup';
/**
* The library settings
*
* @var array
*/
public $library_settings = [];
/**
* Favorites.
*
* @var Faovirtes
*/
public $favorites;
/**
* Templates.
*
* @var Templates
*/
public $templates;
public function __construct($library_settings = [])
{
$this->library_settings = $library_settings;
$this->favorites = new Favorites($this);
$this->templates = new Templates($this);
}
public function init()
{
$this->prepare();
// Enqueue media
$this->register_media();
// Add library popups
$this->add_library_popup();
$this->add_library_item_info_popup();
$this->add_library_preview_template_popup();
}
/**
* Prepares the Library.
*
* @return void
*/
private function prepare()
{
$this->library_settings['preview_url'] = TF_TEMPLATES_SITE_URL . '?template_preview=1&template=TEMPLATE_ID&project=' . $this->library_settings['project'];
if (!$this->templates->hasFilters())
{
$this->library_settings['class'] = 'no-sidebar';
}
$this->prepareModal();
}
/**
* Adds the toolbar to the modal's header.
*
* @return void
*/
public function prepareModal()
{
// Upgrade to Pro Button
if ($this->getLibrarySetting('project_license_type') === 'lite')
{
?>
<a href="#" style="display:none;" class="tf-button outline red tf-header-upgrade-button" data-pro-only="<?php echo Text::_('NR_PRO_TEMPLATES'); ?>">
<svg class="icon" width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 10C7.5 10.2761 7.72386 10.5 8 10.5C8.27614 10.5 8.5 10.2761 8.5 10L7.5 10ZM8.35355 3.64645C8.15829 3.45118 7.84171 3.45118 7.64645 3.64645L4.46447 6.82843C4.2692 7.02369 4.2692 7.34027 4.46447 7.53553C4.65973 7.7308 4.97631 7.7308 5.17157 7.53553L8 4.70711L10.8284 7.53553C11.0237 7.7308 11.3403 7.7308 11.5355 7.53553C11.7308 7.34027 11.7308 7.02369 11.5355 6.82843L8.35355 3.64645ZM8.5 10L8.5 4L7.5 4L7.5 10L8.5 10Z" fill="currentColor"/>
<path d="M14 7C14 10.3137 11.3137 13 8 13C4.68629 13 2 10.3137 2 7C2 3.68629 4.68629 1 8 1C11.3137 1 14 3.68629 14 7Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<?php echo Text::_('NR_UPGRADE_TO_PRO'); ?>
</a>
<?php
}
// Main Library Modal Header Toolbar
?>
<div style="display:none;" class="actions-wrapper tfTemplatesLibraryModalToolbar">
<ul class="actions">
<li>
<a href="<?php echo $this->library_settings['create_new_template_link']; ?>" title="<?php echo Text::_('NR_START_FROM_SCRATCH'); ?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8" stroke="currentColor"/>
<line x1="11.9277" y1="8.5" x2="11.9277" y2="15.5" stroke="currentColor" stroke-linecap="round"/>
<line x1="15.5" y1="11.9285" x2="8.5" y2="11.9285" stroke="currentColor" stroke-linecap="round"/>
</svg>
</a>
</li>
<li>
<a href="#" class="tf-templates-refresh-btn" title="<?php echo Text::_('NR_REFRESH_TEMPLATES'); ?>">
<svg class="checkmark" width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" stroke-width="5" />
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.2879 20 16.3514 19.0396 17.8095 17.5" stroke="currentColor" stroke-linecap="round"/>
<path class="tip" d="M22.25 9.99999L20 12.25L17.75 9.99999" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
</li>
<li>
<a href="https://www.tassos.gr/contact?topic=Custom Development&extension=<?php echo $this->library_settings['project_name']; ?>" title="<?php echo Text::_('NR_REQUEST_TEMPLATE'); ?>" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.8 16H9.3C9.3 15.7239 9.07614 15.5 8.8 15.5V16ZM8.8 20H8.3C8.3 20.1905 8.40823 20.3644 8.57912 20.4486C8.75002 20.5327 8.95387 20.5125 9.10486 20.3963L8.8 20ZM13.7304 16.2074L14.0353 16.6037L13.7304 16.2074ZM5 4.5H19V3.5H5V4.5ZM19.5 5V15H20.5V5H19.5ZM4.5 15V5H3.5V15H4.5ZM8.8 15.5H5V16.5H8.8V15.5ZM9.3 20V16H8.3V20H9.3ZM19 15.5H14.3401V16.5H19V15.5ZM13.4256 15.8111L8.49514 19.6037L9.10486 20.3963L14.0353 16.6037L13.4256 15.8111ZM3.5 15C3.5 15.8284 4.17157 16.5 5 16.5V15.5C4.72386 15.5 4.5 15.2761 4.5 15H3.5ZM19.5 15C19.5 15.2761 19.2761 15.5 19 15.5V16.5C19.8284 16.5 20.5 15.8284 20.5 15H19.5ZM14.3401 15.5C14.0093 15.5 13.6878 15.6094 13.4256 15.8111L14.0353 16.6037C14.1227 16.5365 14.2299 16.5 14.3401 16.5V15.5ZM19 4.5C19.2761 4.5 19.5 4.72386 19.5 5H20.5C20.5 4.17157 19.8284 3.5 19 3.5V4.5ZM5 3.5C4.17157 3.5 3.5 4.17157 3.5 5H4.5C4.5 4.72386 4.72386 4.5 5 4.5V3.5Z" fill="currentColor"/>
</svg>
</a>
</li>
<li>
<a href="#" class="tf-templates-library-toggle-fullscreen">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 2H14V7" stroke="currentColor" stroke-width="1"/>
<path d="M7 14L2 14L2 9" stroke="currentColor" stroke-width="1"/>
</svg>
<svg class="on-fullscreen" width="16" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 5L9 5L9 3.97232e-08" stroke="currentColor" stroke-width="1"/>
<path d="M0 9H5L5 14" stroke="currentColor" stroke-width="1"/>
</svg>
</a>
</li>
<li>
<a href="#" class="tf-modal-close" data-bs-dismiss="modal" data-dismiss="modal">
<svg height="14" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="12.5933" width="2.47487" height="17.3241" transform="rotate(135 14 12.5933)" fill="currentColor"/>
<rect width="2.47487" height="17.3241" transform="matrix(-0.707109 -0.707105 0.707109 -0.707105 1.75 14.3433)" fill="currentColor"/>
</svg>
</a>
</li>
</ul>
</div>
<?php // Templates Library Info Popup Header Toolbar ?>
<div style="display:none;" class="actions-wrapper tfInfoTemplatesLibraryModalToolbar">
<ul class="actions">
<li>
<a href="#" class="tf-modal-close" data-bs-dismiss="modal" data-dismiss="modal">
<svg height="14" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="12.5933" width="2.47487" height="17.3241" transform="rotate(135 14 12.5933)" fill="currentColor"/>
<rect width="2.47487" height="17.3241" transform="matrix(-0.707109 -0.707105 0.707109 -0.707105 1.75 14.3433)" fill="currentColor"/>
</svg>
</a>
</li>
</ul>
</div>
<?php // Templates Library Preview Popup Header Toolbar ?>
<div style="display:none;" class="actions-wrapper tfPreviewTemplatesLibraryModalToolbar">
<ul class="actions">
<li>
<a href="#" class="tf-modal-close" data-bs-dismiss="modal" data-dismiss="modal">
<svg height="14" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="12.5933" width="2.47487" height="17.3241" transform="rotate(135 14 12.5933)" fill="currentColor"/>
<rect width="2.47487" height="17.3241" transform="matrix(-0.707109 -0.707105 0.707109 -0.707105 1.75 14.3433)" fill="currentColor"/>
</svg>
</a>
</li>
</ul>
</div>
<?php // Templates Library Preview Popup Actions on the left side of the header ?>
<div style="display:none;" class="modal-title-wrapper tfPreviewTemplatesLibraryModalToolbarLeft">
<a href="#" class="tf-modal-close tf-templates-library-preview-go-back" data-bs-dismiss="modal" data-dismiss="modal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4L8 12L16 20" stroke="currentColor" stroke-linecap="round"/>
</svg>
<span class="tf-back"><?php echo Text::_('NR_BACK'); ?></span>
</a>
<a href="#" class="tf-templates-library-refresh-demo">
<?php echo $this->getRefreshIcon(); ?>
</a>
<h3 class="modal-title"></h3>
</div>
<?php // Templates Library Preview Popup responsive control actions in the middle of the header ?>
<div style="display:none;" class="tf-templates-library-preview-responsive-devices tfPreviewTemplatesLibraryModalToolbarCenter">
<svg class="tf-templates-library-preview-responsive-device active" data-device="desktop" width="35" height="24" viewBox="0 0 35 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="33" height="19" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M16 21.5V21C16 20.4477 16.4477 20 17 20H19C19.5523 20 20 20.4477 20 21V21.5C20 22.0523 20.4477 22.5 21 22.5H23.25C23.6642 22.5 24 22.8358 24 23.25C24 23.6642 23.6642 24 23.25 24H12.75C12.3358 24 12 23.6642 12 23.25C12 22.8358 12.3358 22.5 12.75 22.5H15C15.5523 22.5 16 22.0523 16 21.5Z" fill="currentColor"/>
</svg>
<svg class="tf-templates-library-preview-responsive-device" data-device="tablet" width="19" height="24" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="17" height="22" rx="2" stroke="currentColor" stroke-width="2"/>
<circle cx="9.5" cy="19.5" r="1.5" fill="currentColor"/>
</svg>
<svg class="tf-templates-library-preview-responsive-device" data-device="mobile" width="15" height="24" viewBox="0 0 15 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="13" height="22" rx="2" stroke="currentColor" stroke-width="2"/>
<line x1="5" y1="2" x2="10" y2="2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<?php
/**
* Add the necessary HTML for each modal's header by using the
* main popup ID given by the initiator extension.
*/
Factory::getDocument()->addScriptDeclaration('
document.addEventListener("DOMContentLoaded", function() {
/**
* Main Templates Library Popup
*/
let mainPopup = document.querySelector("#' . $this->library_settings['id'] . '");
/**
* Append Upgrade to Pro button to header
*/
let upgradeButton = document.querySelector(".tf-header-upgrade-button");
if (upgradeButton) {
upgradeButton.removeAttribute("style");
mainPopup.querySelector(".modal-header").append(upgradeButton);
}
// Append actions
let modalToolbar = document.querySelector(".tfTemplatesLibraryModalToolbar")
modalToolbar.removeAttribute("style");
mainPopup.querySelector(".modal-header").append(modalToolbar);
// Add class to library popup
mainPopup.classList.add("tf-templates-library", "tf-templates-library-popup", "' . (defined('nrJ4') ? 'isJ4' : 'isJ3') . '");
/**
* Info Templates Library Popup
*/
let infoPopup = document.querySelector("#' . $this->info_modal_id . '");
// Append actions
modalToolbar = document.querySelector(".tfInfoTemplatesLibraryModalToolbar")
modalToolbar.removeAttribute("style");
infoPopup.querySelector(".modal-header").append(modalToolbar);
// Add class to info popup
infoPopup.classList.add("tf-templates-library-item-info", "tf-templates-library-popup", "' . (defined('nrJ4') ? 'isJ4' : 'isJ3') . '");
/**
* Preview Templates Library Popup
*/
let previewPopup = document.querySelector("#' . $this->preview_modal_id . '");
// Append toolbar on the left side of the header
modalToolbar = document.querySelector(".tfPreviewTemplatesLibraryModalToolbarLeft").cloneNode(true);
modalToolbar.removeAttribute("style");
previewPopup.querySelector(".modal-header").insertBefore(modalToolbar, previewPopup.querySelector(".modal-header").firstChild);
// Append responsive icons on the center of the header
modalToolbar = document.querySelector(".tfPreviewTemplatesLibraryModalToolbarCenter").cloneNode(true);
modalToolbar.removeAttribute("style");
previewPopup.querySelector(".modal-header").append(modalToolbar);
// Append actions
modalToolbar = document.querySelector(".tfPreviewTemplatesLibraryModalToolbar")
modalToolbar.removeAttribute("style");
previewPopup.querySelector(".modal-header").append(modalToolbar);
// Add class to preview popup
previewPopup.classList.add("tf-templates-library-popup-preview", "tf-templates-library-popup", "' . (defined('nrJ4') ? 'isJ4' : 'isJ3') . '");
});
');
}
/**
* Adds admin media
*
* @return void
*/
public function register_media()
{
// Templates Library CSS
HTMLHelper::stylesheet('plg_system_nrframework/tf_templates_library.css', ['relative' => true, 'version' => 'auto']);
// Templates Library JS
HTMLHelper::script('plg_system_nrframework/tf_templates_library.js', ['relative' => true, 'version' => 'auto']);
// Add Javascript options
$doc = Factory::getDocument();
$options = $doc->getScriptOptions('tassos_framework');
$options = is_array($options) ? $options : [];
$options = [
'project_name' => $this->library_settings['project_name'],
'pro' => Text::_('NR_PRO'),
'lite' => Text::_('NR_LITE'),
'license_key' => Text::_('NR_LICENSE_KEY'),
'license' => $this->library_settings['license_key'],
'install_extension' => TEXT::_('NR_INSTALL_EXTENSION'),
'update_extension' => TEXT::_('NR_UPDATE_EXTENSION'),
'templates_library_ajax_url' => Uri::base() . '?option=com_ajax&format=raw&plugin=nrframework&task=TemplatesLibrary',
'csrf_token' => Session::getFormToken()
];
$doc->addScriptOptions('tassos_framework', $options);
}
/**
* Adds the popup at the footer of the page. Appears when you click the "New" / "Add New" button.
*
* @return void
*/
public function add_library_popup()
{
$payload = [
'title' => $this->library_settings['title'],
'closeButton' => false,
'backdrop' => 'static'
];
$content = LayoutHelper::render('library/tmpl', $this->library_settings, JPATH_PLUGINS . '/system/nrframework/layouts');
echo HTMLHelper::_('bootstrap.renderModal', $this->library_settings['id'], $payload, $content);
}
/**
* Adds the popup that displays the info for each template.
*
* @return void
*/
public function add_library_item_info_popup()
{
$info_payload = [
'category_label' => $this->library_settings['main_category_label']
];
$content = LayoutHelper::render('library/info_popup', $info_payload, JPATH_PLUGINS . '/system/nrframework/layouts');
$payload = [
'title' => 'Template Title',
'closeButton' => false,
'backdrop' => 'static'
];
echo HTMLHelper::_('bootstrap.renderModal', $this->info_modal_id, $payload, $content);
}
/**
* Adds the popup at that allows us to preview a template.
*
* @return void
*/
public function add_library_preview_template_popup()
{
$content = LayoutHelper::render('library/preview', [], JPATH_PLUGINS . '/system/nrframework/layouts');
$payload = [
'title' => 'Template Title',
'closeButton' => false,
'backdrop' => 'static'
];
echo HTMLHelper::_('bootstrap.renderModal', $this->preview_modal_id, $payload, $content);
}
/**
* Return templates folder path
*
* @return string
*/
public function getTemplatesPath()
{
$component = isset($this->library_settings['component']) ? $this->library_settings['component'] : 'com_rstbox';
return JPATH_ROOT . '/media/' . $component . '/templates/';
}
/**
* Returns the Framework Plugin URL.
*
* @return string
*/
public function getNRFrameworkPluginURL()
{
return Uri::base() . 'index.php?option=com_plugins&task=plugin.edit&extension_id=' . \NRFramework\Extension::getID('nrframework', 'plugin', 'system');
}
/**
* Returns a library settings value.
*
* @param string $key
* @param string $default
*
* @return string
*/
public function getLibrarySetting($key, $default = '')
{
return isset($this->library_settings[$key]) ? $this->library_settings[$key] : $default;
}
/**
* Sets a library settings value.
*
* @param string $key
* @param mixed $value
*
* @return string
*/
public function setLibrarySetting($key, $value)
{
$this->library_settings[$key] = $value;
}
/**
* Returns the refresh icon.
*
* @return string
*/
public function getRefreshIcon()
{
return '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.2879 20 16.3514 19.0396 17.8095 17.5" stroke="currentColor" stroke-linecap="round"/>
<path d="M22.25 9.99999L20 12.25L17.75 9.99999" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>';
}
} Library/Templates.php 0000644 00000021470 15235314576 0010636 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Library;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Layout\LayoutHelper;
class Templates
{
/**
* Library
*
* @var Library
*/
protected $library = [];
/**
* The user download key.
*
* @var String
*/
private $download_key = null;
public function __construct($library = [])
{
$this->library = $library;
$this->download_key = $this->library->getLibrarySetting('license_key');
}
/**
* Checks whether we have the template locally and retrives its layout.
* If no local template is found, then retrieves it from remote and returns its layout.
*
* @return string
*/
public function tf_library_ajax_get_templates()
{
return $this->getTemplates($this->getList());
}
/**
* Checks whether the given license is valid or not and updates the "license_key_status" property
* which defines whether the pro templates should contain an error letting the user know that their
* license is invalid.
*
* @return void
*/
private function checkAndUpdateLicenseStatus()
{
$license_status = \NRFramework\Helpers\License::getRemoteLicenseData($this->download_key);
$this->library->setLibrarySetting('license_key_status', !isset($license_status['error']) ? 'valid' : 'invalid');
}
/**
* Returns all available templates
*
* @param array $templates
*
* @return array
*/
private function getTemplates($templates = [])
{
if (isset($templates->error) && $templates->error)
{
return $templates;
}
$this->checkAndUpdateLicenseStatus();
$layout_payload = [
'main_category_label' => $this->library->getLibrarySetting('main_category_label'),
'project_name' => $this->library->getLibrarySetting('project_name'),
'project_license_type' => $this->library->getLibrarySetting('project_license_type'),
'project_version' => $this->library->getLibrarySetting('project_version'),
'product_license_settings_url' => $this->library->getLibrarySetting('product_license_settings_url'),
'template_use_url' => $this->library->getLibrarySetting('template_use_url'),
'license_key' => $this->download_key,
'license_key_status' => $this->library->getLibrarySetting('license_key_status'),
'templates' => isset($templates->templates) ? $templates->templates : [],
'favorites' => $this->library->favorites->getFavorites()
];
$filters_payload = [
'filters' => $this->getTemplatesFilters(isset($templates->filters) ? $templates->filters : [])
];
$layouts_path = JPATH_PLUGINS . '/system/nrframework/layouts';
return [
'templates' => LayoutHelper::render('library/items_list', $layout_payload, $layouts_path),
'filters' => LayoutHelper::render('library/filters', $filters_payload, $layouts_path)
];
}
/**
* Returns the filters payload.
*
* @param object $filters
*
* @return array
*/
private function getTemplatesFilters($filters)
{
// Main filters
$data = [];
$categories = isset($filters->categories) ? $filters->categories : [];
if ($categories)
{
$data['category'] = [
'label' => $this->library->getLibrarySetting('main_category_label', Text::_('NR_CATEGORIES_PLURAL')),
'items' => $categories
];
}
$goals = isset($filters->goals) ? $filters->goals : [];
if ($goals)
{
$data['goal'] = [
'label' => Text::_('NR_GOALS'),
'items' => $goals
];
}
// Add compatibility filter (Free/Pro filtering) only in the Lite version
if ($this->library->getLibrarySetting('project_license_type') === 'lite')
{
$compatibility = isset($filters->compatibility) ? $filters->compatibility : [];
if ($compatibility)
{
$data['compatibility'] = [
'label' => Text::_('NR_COMPATIBILITY'),
'items' => $compatibility
];
}
}
return $data;
}
public function hasFilters()
{
if (!$localTemplates = $this->getLocalTemplates())
{
return;
}
if (!isset($localTemplates->filters))
{
return;
}
$categories = isset($localTemplates->filters->categories) ? $localTemplates->filters->categories : [];
$goals = isset($localTemplates->filters->goals) ? $localTemplates->filters->goals : [];
$isFree = $this->library->getLibrarySetting('project_license_type') === 'lite';
return $categories || $goals || $isFree;
}
/**
* Retrieve remote templates, store them locally and return new layout.
*
* @return string
*/
public function tf_library_ajax_refresh_templates()
{
return $this->getTemplates($this->getRemoteTemplatesAndStore());
}
/**
* Insert template.
*
* @return void
*/
public function tf_library_ajax_insert_template()
{
$template_id = $this->library->getLibrarySetting('template_id');
// Get remote template
$templates_url = str_replace('{{PROJECT}}', $this->library->getLibrarySetting('project'), TF_TEMPLATE_GET_URL);
$templates_url = str_replace('{{DOWNLOAD_KEY}}', $this->download_key, $templates_url);
$templates_url = str_replace('{{TEMPLATE}}', $template_id, $templates_url);
$response = HttpFactory::getHttp()->get($templates_url);
if (!$body = json_decode($response->body, true))
{
return [
'error' => true,
'message' => 'Cannot insert template.'
];
}
// An error has occurred
if (isset($body['error']) && $body['error'])
{
return [
'error' => true,
'message' => $body['response']
];
}
// Prepare template
$template = $body['response']['template'];
// Set ID used to check if we are adding a valid template within the extension's item edit page
$template['id'] = $body['response']['id'];
// Save template locally so we can fetch its contents on redirect
file_put_contents($this->library->getTemplatesPath() . 'template.json', json_encode($template));
return [
'error' => false,
'message' => 'Inserting template.',
'redirect' => $this->library->getLibrarySetting('template_use_url') . $template_id
];
}
/**
* Save templates locally
*
* @param array $body
*
* @return void
*/
private function saveLocalTemplate($body)
{
// Create directory if not exist
if (!is_dir($this->library->getTemplatesPath()))
{
\NRFramework\File::createDirs($this->library->getTemplatesPath());
}
$path = $this->library->getTemplatesPath() . 'templates.json';
file_put_contents($path, json_encode($body));
}
/**
* Returns the local templates
*
* @return array
*/
private function getLocalTemplates()
{
$path = $this->library->getTemplatesPath() . 'templates.json';
if (!file_exists($path))
{
return false;
}
// If templates are old, fetch remote list
if ($this->templatesRequireUpdate())
{
return false;
}
return json_decode(file_get_contents($path));
}
/**
* Checks whether the local templates list is older than X days.
*
* @return bool
*/
private function templatesRequireUpdate()
{
$path = $this->library->getTemplatesPath() . 'templates.json';
$days_old = 7;
/**
* If its older than X days, then request remote list
*/
// Get the modification time of the templates file
$modTime = @filemtime($path);
// Current time
$now = time();
// Minimum time difference
$threshold = $days_old * 24 * 3600;
// Do we need an update?
return ($now - $modTime) >= $threshold;
}
/**
* Returns the remote templates
*
* @return array
*/
private function getRemoteTemplates()
{
// Get remote templates
$templates_url = str_replace('{{PROJECT}}', $this->library->getLibrarySetting('project'), TF_TEMPLATES_GET_URL);
$response = HttpFactory::getHttp()->get($templates_url);
if (!$response = $response->body)
{
return;
}
if (!$response = json_decode($response))
{
$error = new \stdClass();
$error->error = true;
$error->message = sprintf(Text::_('NR_TEMPLATES_CANNOT_BE_RETRIEVED'), $this->library->getRefreshIcon());
return $error;
}
return $response;
}
/**
* Gets the remote templates and stores them locally
*
* @return array
*/
private function getRemoteTemplatesAndStore()
{
$templates = $this->getRemoteTemplates();
if (isset($templates->error) && $templates->error)
{
return $templates;
}
$this->saveLocalTemplate($templates);
return $templates;
}
/**
* Get templates list
*
* @return array
*/
private function getList()
{
// try to find local templates with fallback remote templates
return $this->getLocalTemplates() ?: $this->getRemoteTemplatesAndStore();
}
} Library/Favorites.php 0000644 00000005437 15235314576 0010647 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Library;
defined('_JEXEC') or die;
class Favorites
{
/**
* Library
*
* @var Library
*/
protected $library = [];
public function __construct($library = [])
{
$this->library = $library;
}
/**
* Handles AJAX Library favorite toggle
*
* @return string
*/
public function tf_library_ajax_favorites_toggle()
{
$template_id = $this->library->getLibrarySetting('template_id');
if (empty($template_id))
{
return false;
}
$this->addOrRemoveFavorite($template_id);
return $this->getFavorites();
}
/**
* Add or remove favorites
*
* @param int $template_id
*
* @return void
*/
private function addOrRemoveFavorite($template_id)
{
$favorites = $this->getFavorites();
if (array_key_exists($template_id, $favorites))
{
$this->removeFromFavorites($template_id);
return;
}
$this->addToFavorites($template_id);
}
/**
* Add to favorites
*
* @param int $template_id
*
* @return void
*/
private function addToFavorites($template_id)
{
$favorites = $this->getFavorites();
if (array_key_exists($template_id, $favorites))
{
return;
}
$favorites[$template_id] = true;
$this->saveFavorites($favorites);
}
/**
* Save favorites to file
*
* @param string $content
*
* @return void
*/
private function saveFavorites($content)
{
// Create directory if not exist
if (!is_dir($this->library->getTemplatesPath()))
{
\NRFramework\File::createDirs($this->library->getTemplatesPath());
}
$file = $this->library->getTemplatesPath() . 'favorites.json';
return file_put_contents($file, json_encode($content));
}
/**
* Remove from favorites
*
* @param int $template_id
*
* @return void
*/
private function removeFromFavorites($template_id)
{
$favorites = $this->getFavorites();
unset($favorites[$template_id]);
$this->saveFavorites($favorites);
}
/**
* Get favorites
*
* @return array
*/
public function getFavorites()
{
$file = $this->library->getTemplatesPath() . 'favorites.json';
if (!file_exists($file))
{
return [];
}
return (array) json_decode(file_get_contents($file), true);
}
} Widgets/Vimeo.php 0000644 00000007563 15235314576 0007770 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
class Vimeo extends Video
{
/**
* Widget default options
*
* @var array
*/
protected $video_widget_options = [
/**
* Set the cover image type.
*
* Allowed Values:
* - none
* - auto
* - custom
*/
'coverImageType' => 'none',
// The Cover Image URL when coverImage="custom"
'coverImage' => '',
// Whether controls will appear in the video
'controls' => true,
// Loop
'loop' => false,
// Mute
'mute' => false,
/**
* Set whether to load the video in privacy-enhanced mode.
*
* When this is enabled, Vimeo will block the player from tracking
* any session data, including all cookies and analytics.
*/
'privacy' => false,
// Whether to show the video title
'title' => false,
// Whether to show the author of the video
'byline' => false,
// Whether to show the author's profile image
'portrait' => false,
// The color of the video controls
'color' => '#00adef',
// Whether to allow keyboard inputs
'keyboard' => false,
// Enable to show the picture-in-picture button in the control bar
'pip' => false,
// Set the start time
'start' => null,
// Set the end time
'end' => null,
];
/**
* Prepares the widget.
*
* @return void
*/
protected function prepare()
{
$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
// Abort
if ($videoProvider !== 'vimeo')
{
$this->options['value'] = null;
return;
}
$this->options['css_class'] .= ' vimeo';
$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';
if ($this->options['coverImageType'] === 'auto')
{
$this->options['coverImage'] = 'url("https://vumbnail.com/' . $videoID . '.jpg")';
}
else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
{
$coverImage = explode('#', $this->options['coverImage']);
$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
}
$atts = [
'data-video-id="' . $videoID . '"',
'data-video-type="' . $videoProvider . '"',
'data-video-mute="' . var_export($this->options['mute'], true) . '"',
'data-video-controls="' . var_export($this->options['controls'], true) . '"',
'data-video-loop="' . var_export($this->options['loop'], true) . '"',
'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
'data-video-privacy="' . var_export($this->options['privacy'], true) . '"',
'data-video-title="' . var_export($this->options['title'], true) . '"',
'data-video-byline="' . var_export($this->options['byline'], true) . '"',
'data-video-portrait="' . var_export($this->options['portrait'], true) . '"',
'data-video-keyboard="' . var_export($this->options['keyboard'], true) . '"',
'data-video-pip="' . var_export($this->options['pip'], true) . '"',
'data-video-color="' . $this->options['color'] . '"',
'data-video-start="' . $this->options['start'] . '"',
'data-video-end="' . $this->options['end'] . '"',
];
$this->options['atts'] = implode(' ', $atts);
}
/**
* We use the video widget layout file.
*
* @return string
*/
public function getName()
{
return 'video';
}
/**
* Loads media files
*
* @return void
*/
public function videoAssets()
{
HTMLHelper::script('plg_system_nrframework/widgets/video/vimeo.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Video.php 0000644 00000004503 15235314576 0007746 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
abstract class Video extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// Video URL
'value' => '',
// Video width
'width' => '480px',
// Video height
'height' => '270px',
// Whether the video will autoplay
'autoplay' => false,
// Whether the video will autopause whenever we scroll and it hides from our viewport
'autopause' => false
];
public function __construct($options = [])
{
$this->widget_options = array_merge($this->widget_options, $this->video_widget_options);
parent::__construct($options);
$this->prepare();
$this->styles();
}
protected function prepare()
{}
public function render()
{
$this->loadMedia();
return parent::render();
}
public function styles()
{
if (!$this->options['load_css_vars'])
{
return;
}
$controls = [
[
'property' => '--video-width',
'value' => $this->options['width']
],
[
'property' => '--video-height',
'value' => $this->options['height']
]
];
$selector = '.nrf-widget.tf-video.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
Factory::getDocument()->addStyleDeclaration($controlsCSS);
}
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
if ($this->options['load_stylesheet'])
{
HTMLHelper::stylesheet('plg_system_nrframework/widgets/video.css', ['relative' => true, 'version' => 'auto']);
}
HTMLHelper::script('plg_system_nrframework/widgets/video.js', ['relative' => true, 'version' => 'auto']);
if (method_exists($this, 'videoAssets'))
{
$this->videoAssets();
}
HTMLHelper::script('plg_system_nrframework/widgets/videos.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Gallery.php 0000644 00000063164 15235314576 0010307 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
use NRFramework\Mimes;
use NRFramework\File;
use NRFramework\Image;
/**
* Gallery
*/
class Gallery extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The gallery items source.
*
* This can be one or combination of the following:
*
* - Path to a relative folder (String)
* /path/to/folder
* - Path to a relative image (String)
* /path/to/folder/image.png
* - URL of an image (String)
* https://example.com/path/to/image.png
* - Array of images (Array)
* [
* 'url' => 'https://example.com/path/to/image.png',
* 'thumbnail_url' => 'https://example.com/path/to/image_thumb.png',
* 'caption' => 'This is a caption',
* 'thumbnail_size' => [
* 'width' => '200',
* 'height' => '200'
* ],
* 'module' => 'position-2'
* ]
*
* - The `url` property is required.
* - All other properties are optional.
*/
'items' => [],
/**
* Set the ordering.
*
* Available values:
* - default
* - alphabetical
* - reverse_alphabetical
* - random
*/
'ordering' => 'default',
// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
'module' => '',
/**
* Set the style of the gallery:
*
* - masonry
* - grid
* - justified
*/
'style' => 'masonry',
// Each item height (in pixels) in Justified layout
'justified_item_height' => null,
/**
* Define the columns per supported device.
*
* Example value:
* - An integer representing the columns for all devices: 3
* - A value for each device:
* [
* 'desktop' => 3,
* 'tablet' => 2,
* 'mobile' => 1
* ]
*/
'columns' => 4,
/**
* Define the gap per gallery item per supported device.
*
* Example value:
* - An integer representing the gap for all devices: 30
* - A value for each device:
* [
* 'desktop' => 30,
* 'tablet' => 20,
* 'mobile' => 10
* ]
*/
'gap' => 15,
/**
* Set the allowed file types.
*
* This is used to validate the files loaded via a directory or a fixed path to an image.
*
* Given URLs are not validated by this setting.
*/
'allowed_file_types' => '.jpg, .jpeg, .png',
// Gallery Items wrapper CSS classes
'gallery_items_css' => '',
// Set whether to display a lightbox
'lightbox' => false,
/**
* Source Image
*/
/**
* Should the source image be resized?
*
* If `original_image_resize` is false, then the source image will appear
* in the lightbox (also if `thumbnails` is false, the source image will also appear as the thumbnail)
*
* Issue: if this image is a raw photo, there are chances it will increase the page load in order for the browser to display the image.
*
* By enabling this, we resize the source image to our desired dimensions and reduce the page load in the above scenario.
*
* Note: Always ensure the source image is backed up to a safe place.
* Note 2: We require thumbnails or original image resize to be enabled for this to work.
* Reason: The above options if enabled generate the gallery_info.txt file in the /cache folder which helps us
* generate the source images only if necessary(image has been edited), otherwise, the source image would
* be generated on each page refresh.
*/
'source_image_resize' => false,
// Source image resize width
'source_image_resize_width' => 1920,
// Source image resize height
'source_image_resize_height' => null,
// Source image resize method (crop, stretch, fit)
'source_image_resize_method' => 'crop',
// Source image resize quality
'source_image_resize_image_quality' => 80,
/**
* Original Image
*/
// Should the original uploaded image be resized?
'original_image_resize' => false,
// Resize method (crop, stretch, fit)
'original_image_resize_method' => 'crop',
/**
* Original Image Resize Width.
*
* If `original_image_resize_height` is null, resizes via the width to keep the aspect ratio.
*/
'original_image_resize_width' => 1920,
// Original Image Resize Height
'original_image_resize_height' => null,
// Original Image Resize Quality
'original_image_resize_image_quality' => 80,
/**
* Thumbnails
*/
// Set whether to generate thumbnails on-the-fly
'thumbnails' => false,
// Resize method (crop, stretch, fit)
'thumb_resize_method' => 'crop',
// Thumbnails width
'thumb_width' => 300,
// Thumbnails height
'thumb_height' => null,
// The CSS class of the thumbnail
'thumb_class' => '',
/**
* Set whether to resize the images whenever their source file changes.
*
* i.e. If we edit the source image and also need to recreate the resized original image or thumbnail.
* This is rather useful otherwise we would have to delete the resized image or thumbnail in order for it to be recreated.
*/
'force_resizing' => false,
// Destination folder
'destination_folder' => 'cache/tassos/gallery',
// Attributes set to the wrapper
'atts' => '',
// The unique hash of this gallery based on its options
'hash' => null,
// Set whether to show warnings when an image that has been set to appear does not exist.
'show_warnings' => true,
/**
* This is a list that's populated
* automatically by looking for tags
* in each gallery item.
*/
'tags' => [],
/**
* Set the tags position.
*
* Available values:
* - disabled (No tags will appear in the gallery)
* - above
* - below
*/
'tags_position' => 'disabled',
/**
* Set the tags ordering.
*
* Available values:
* - default
* - alphabetical
* - reverse_alphabetical
* - random
*/
'tags_ordering' => 'default',
// Set the label of the "All Tags" option
'all_tags_item_label' => 'All',
/**
* Set whether to show the tags filter on mobile devices,
* show them as a dropdown or disable them.
*
* Available values:
* - show
* - dropdown
* - disabled
*/
'tags_mobile' => 'show',
'tags_text_color' => '#555',
'tags_text_color_hover' => '#fff',
'tags_bg_color_hover' => '#1E3148',
// Widget Custom CSS
'custom_css' => ''
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
/**
* Prepares the Gallery.
*
* @return void
*/
private function prepare()
{
$this->options['hash'] = $this->getHash();
$this->options['destination_folder'] = JPATH_ROOT . DIRECTORY_SEPARATOR . $this->options['destination_folder'] . DIRECTORY_SEPARATOR . $this->options['hash'] . DIRECTORY_SEPARATOR;
$this->parseGalleryItems();
$this->cleanDestinationFolder();
$this->resizeSourceImages();
$this->resizeOriginalImages();
$this->createThumbnails();
// Set style on the gallery items container.
$this->options['gallery_items_css'] .= ' ' . $this->getStyle();
// Set class to trigger lightbox.
if ($this->options['lightbox'])
{
$this->options['css_class'] .= ' lightbox';
}
$this->setAtts();
$this->prepareItems();
$this->setOrdering();
if ($this->options['load_css_vars'])
{
$this->options['custom_css'] = $this->getWidgetCSS();
}
$this->prepareTags();
}
/**
* Sets the data attributes.
*
* @return void
*/
private function setAtts()
{
$atts = [];
$atts[] = 'data-id="' . $this->options['id'] . '"';
if ($this->options['style'] === 'justified' && $this->options['justified_item_height'])
{
$atts[] = 'data-item-height="' . $this->options['justified_item_height'] . '"';
}
$this->options['atts'] = implode(' ', $atts);
}
/**
* Sets the ordering of the gallery.
*
* @return void
*/
private function setOrdering()
{
switch ($this->options['ordering']) {
case 'random':
shuffle($this->options['items']);
break;
case 'alphabetical':
usort($this->options['items'], [$this, 'compareByThumbnailASC']);
break;
case 'reverse_alphabetical':
usort($this->options['items'], [$this, 'compareByThumbnailDESC']);
break;
}
}
/**
* Compares tag names in ASC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
public function compareByTagNameASC($a, $b)
{
return strcmp($a, $b);
}
/**
* Compares tag names in DESC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
public function compareByTagNameDESC($a, $b)
{
return strcmp($b, $a);
}
/**
* Compares thumbnail file names in ASC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
public function compareByThumbnailASC($a, $b)
{
return strcmp(basename($a['thumbnail']), basename($b['thumbnail']));
}
/**
* Compares thumbnail file names in DESC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
public function compareByThumbnailDESC($a, $b)
{
return strcmp(basename($b['thumbnail']), basename($a['thumbnail']));
}
/**
* Get the hash of this gallery.
*
* Generate the hash with only the essential options of the Gallery widget.
* i.e. with the data that are related to the images.
*
* @return string
*/
private function getHash()
{
$opts = [
'items',
'style',
'allowed_file_types',
'source_image_resize',
'source_image_resize_width',
'source_image_resize_height',
'source_image_resize_method',
'source_image_resize_image_quality',
'original_image_resize',
'original_image_resize_method',
'original_image_resize_width',
'original_image_resize_height',
'original_image_resize_image_quality',
'thumbnails',
'thumb_resize_method',
'thumb_width',
'thumb_height',
'force_resizing',
'destination_folder'
];
$payload = [];
foreach ($opts as $opt)
{
$payload[$opt] = $this->options[$opt];
}
return md5(serialize($payload));
}
/**
* Cleans the source folder.
*
* If an image from the source folder is removed, we also remove the
* original image/thumbnail from the destination folder as well as
* from the gallery info file.
*
* @return void
*/
private function cleanDestinationFolder()
{
if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
{
return;
}
// Find all folders that we need to search
$dirs_to_search = [];
// Store all source files
$source_files = [];
foreach ($this->options['items'] as $key => $item)
{
if (!isset($item['path']))
{
continue;
}
$source_files[] = pathinfo($item['path'], PATHINFO_BASENAME);
$directory = is_dir($item['path']) ? $item['path'] : dirname($item['path']);
if (in_array($directory, $dirs_to_search))
{
continue;
}
$dirs_to_search[] = $directory;
}
if (empty($dirs_to_search))
{
return;
}
// Loop each directory found and check which files we need to delete
foreach ($dirs_to_search as $dir)
{
$source_folder_info_file = GalleryHelper::getGalleryInfoFileData($dir);
// Find all soon to be deleted files
$to_be_deleted = array_diff(array_keys($source_folder_info_file), $source_files);
if (!count($to_be_deleted))
{
continue;
}
foreach ($to_be_deleted as $source)
{
// Original image delete
if (isset($source_folder_info_file[$source]))
{
$file = $this->options['destination_folder'] . $source_folder_info_file[$source]['filename'];
if (file_exists($file))
{
unlink($file);
}
}
// Thumbnail delete
$parts = pathinfo($file);
$thumbnail = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
if (file_exists($thumbnail))
{
unlink($thumbnail);
}
// Also remove the image from the gallery info file.
GalleryHelper::removeImageFromGalleryInfoFile(rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $source);
}
}
}
/**
* Returns the gallery style.
*
* @return string
*/
private function getStyle()
{
$style = $this->options['style'];
if ($style === 'justified')
{
return $style;
}
// Get aspect ratio for source image, original image resized and thumbnail
$thumb_height = intval($this->options['thumb_height']);
$thumb_aspect_ratio = $thumb_height ? intval($this->options['thumb_width']) / $thumb_height : 0;
$source_image_height = intval($this->options['source_image_resize_height']);
$source_image_aspect_ratio = $source_image_height ? intval($this->options['source_image_resize_width']) / $source_image_height : 0;
$original_image_height = intval($this->options['original_image_resize_height']);
$original_image_aspect_ratio = $original_image_height ? intval($this->options['original_image_resize_width']) / $original_image_height : 0;
// Check whether the aspect ratio for thumb and lightbox image are the same and use `masonry` style
$checking_aspect_ratio = $this->options['original_image_resize'] ? $original_image_aspect_ratio : $source_image_aspect_ratio;
if ($thumb_aspect_ratio && $checking_aspect_ratio && $thumb_aspect_ratio === $checking_aspect_ratio)
{
return 'masonry';
}
/**
* If both thumbnail width & height are equal we use the `grid` style.
*/
if ($this->options['thumb_width'] === $this->options['thumb_height'])
{
$style = 'grid';
}
/**
* If the style is grid and we do not have a null or 0 thumb_height set the fade lightbox CSS Class.
*
* This CSS Class tells PhotoSwipe to use the fade transition.
*/
if ($style === 'grid' && (!is_null($this->options['thumb_height']) && $this->options['thumb_height'] !== '0'))
{
$this->options['css_class'] .= ' lightbox-fade';
}
return $style;
}
/**
* Prepare the tags.
*
* @return void
*/
private function prepareTags()
{
if ($this->options['tags_position'] === 'disabled')
{
return;
}
if (!is_array($this->options['items']))
{
return;
}
if ($this->options['all_tags_item_label'])
{
$this->options['all_tags_item_label'] = Text::_($this->options['all_tags_item_label']);
}
$tags = $this->options['tags'];
if (count($tags) === 0)
{
foreach ($this->options['items'] as $key => &$item)
{
if (!isset($item['tags']))
{
continue;
}
if (!is_array($item['tags']))
{
continue;
}
$tags = array_merge($tags, $item['tags']);
}
$tags = array_unique($tags);
}
// Sort tags
switch ($this->options['tags_ordering'])
{
case 'random':
shuffle($tags);
break;
case 'alphabetical':
usort($tags, [$this, 'compareByTagNameASC']);
break;
case 'reverse_alphabetical':
usort($tags, [$this, 'compareByTagNameDESC']);
break;
}
$this->options['tags'] = $tags;
}
/**
* Parses the gallery items by finding all iamges to display from all
* different sources.
*
* @return void
*/
private function parseGalleryItems()
{
// If it's a string, we assume its a path to a folder and we convert it to an array.
$this->options['items'] = (array) $this->options['items'];
$items = [];
foreach ($this->options['items'] as $key => $value)
{
if (!$data = GalleryHelper::parseGalleryItems($value, $this->getAllowedFileTypes()))
{
continue;
}
$items = array_merge($items, $data);
}
// Ensure only unique image paths are used
$items = array_unique($items, SORT_REGULAR);
$this->options['items'] = $items;
}
/**
* Returns the allowed file types in an array format.
*
* @return array
*/
public function getAllowedFileTypes()
{
$types = explode(',', $this->options['allowed_file_types']);
$types = array_filter(array_map('trim', array_map('strtolower', $types)));
return $types;
}
/**
* Resizes the source images.
*
* @return mixed
*/
private function resizeSourceImages()
{
if (!$this->options['source_image_resize'])
{
return;
}
// We require either original image resize or thumbnails to be enabled
if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
{
return;
}
foreach ($this->options['items'] as $key => &$item)
{
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
// If force resizing is disabled, continue
if (!$this->options['force_resizing'])
{
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($image_data['path']))
{
continue;
}
}
}
if (is_null($this->options['source_image_resize_height']))
{
Image::resizeAndKeepAspectRatio(
$source,
$this->options['source_image_resize_width'],
$this->options['source_image_resize_image_quality']
);
}
else
{
Image::resize(
$source,
$this->options['source_image_resize_width'],
$this->options['source_image_resize_height'],
$this->options['source_image_resize_image_quality'],
$this->options['source_image_resize_method']
);
}
}
}
/**
* Resizes the original images.
*
* @return mixed
*/
private function resizeOriginalImages()
{
if (!$this->options['original_image_resize'])
{
return;
}
// Create destination folder if missing
File::createDirs($this->options['destination_folder']);
foreach ($this->options['items'] as $key => &$item)
{
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
$unique = true;
// Path to resized image in destination folder
$destination = $this->options['destination_folder'] . basename($source);
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
// If force resizing is disabled and the original image exists, set the URL of the destination image
if (!$this->options['force_resizing'] && file_exists($image_data['path']))
{
$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($image_data['path']))
{
$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
continue;
}
// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
$unique = false;
// The destination path is the same resized image
$destination = $image_data['path'];
}
}
$original_image_file = is_null($this->options['original_image_resize_height'])
?
Image::resizeAndKeepAspectRatio(
$source,
$this->options['original_image_resize_width'],
$this->options['original_image_resize_image_quality'],
$destination,
$unique
)
:
Image::resize(
$source,
$this->options['original_image_resize_width'],
$this->options['original_image_resize_height'],
$this->options['original_image_resize_image_quality'],
$this->options['original_image_resize_method'],
$destination,
$unique
);
if (!$original_image_file)
{
continue;
}
// Set image URL
$item = array_merge($item, [
'url' => GalleryHelper::directoryImageToURL($original_image_file)
]);
// Update image data in Gallery Info File
GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
}
}
/**
* Creates thumbnails.
*
* If `force_resizing` is enabled, it will re-generate thumbnails under the following cases:
*
* - If a thumbnail does not exist.
* - If the original image has been edited.
*
* @return mixed
*/
private function createThumbnails()
{
if (!$this->options['thumbnails'])
{
return false;
}
// Create destination folder if missing
File::createDirs($this->options['destination_folder']);
foreach ($this->options['items'] as $key => &$item)
{
// Skip items that do not have a path set
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
$unique = true;
$parts = pathinfo($source);
$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
/**
* Use the found original image path to produce the thumb file path.
*
* This is used as we have multiple files with the same which produce file names of _copy_X
* and thus the above $destination will not be valid. Instead, we use the original file name
* to find the thumbnail file.
*/
if ($this->options['original_image_resize'])
{
$parts = pathinfo($image_data['path']);
$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
}
// If force resizing is disabled and the thumbnail exists, set the URL of the destination image
if (!$this->options['force_resizing'] && file_exists($destination))
{
$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($destination))
{
$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
continue;
}
// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
$unique = false;
}
}
// Generate thumbnails
$thumb_file = is_null($this->options['thumb_height'])
?
Image::resizeAndKeepAspectRatio(
$source,
$this->options['thumb_width'],
100,
$destination,
$unique,
true,
'resize'
)
:
Image::resize(
$source,
$this->options['thumb_width'],
$this->options['thumb_height'],
100,
$this->options['thumb_resize_method'],
$destination,
$unique,
true,
'resize'
);
if (!$thumb_file)
{
continue;
}
// Set image thumbnail URL
$item = array_merge($item, [
'thumbnail_url' => GalleryHelper::directoryImageToURL($thumb_file)
]);
// Update image data in Gallery Info File
GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
}
}
/**
* Prepares the items.
*
* - Sets the thumbnails image dimensions.
* - Assures caption property exist.
*
* @return mixed
*/
private function prepareItems()
{
if (!is_array($this->options['items']) || !count($this->options['items']))
{
return;
}
$smartTagsInstance = \NRFramework\SmartTags::getInstance();
foreach ($this->options['items'] as $key => &$item)
{
// Initialize image atts
$item['img_atts'] = '';
// Initializes caption if none given
if (!isset($item['caption']))
{
$item['caption'] = '';
}
if (!isset($item['alt']) || empty($item['alt']))
{
$item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
}
// Replace Smart Tags in alt
$item['alt'] = $smartTagsInstance->replace($item['alt']);
if ($item['caption'])
{
$item['caption'] = $smartTagsInstance->replace($item['caption']);
}
// Ensure a thumbnail is given
if (!isset($item['thumbnail_url']))
{
// If no thumbnail is given, set it to the full image
$item['thumbnail_url'] = $item['url'];
continue;
}
// If the thumbnail size for this item is given, set the image attributes
if (isset($item['thumbnail_size']))
{
$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
continue;
}
}
}
/**
* Returns the CSS for the widget.
*
* @param array $exclude_breakpoints Define breakpoints to exclude their CSS
*
* @return string
*/
public function getWidgetCSS($exclude_breakpoints = [])
{
$controls = [
[
'property' => '--gap',
'value' => $this->options['gap'],
'unit' => 'px'
],
[
'property' => '--tags-text-color',
'value' => $this->options['tags_text_color']
],
[
'property' => '--tags-text-color-hover',
'value' => $this->options['tags_text_color_hover']
],
[
'property' => '--tags-bg-color-hover',
'value' => $this->options['tags_bg_color_hover']
]
];
if ($this->options['style'] !== 'justified')
{
$controls[] = [
'property' => [
'--columns' => '%value_raw%',
'--display-items' => 'grid',
'--image-width' => '100%'
],
'fallback_value' => [
'--display-items' => 'flex',
'--display-items-flex-wrap' => 'wrap',
'--image-width' => 'auto'
],
'value' => $this->options['columns'],
];
}
$selector = '.nrf-widget.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
return $controlsCSS;
}
} Widgets/MapAddressEditorView.php 0000644 00000005324 15235314576 0012727 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
class MapAddressEditorView extends OpenStreetMap
{
/**
* Widget default options
*
* @var array
*/
protected $_widget_options = [
// Whether autocomplete is enabled for the address field
'autocomplete' => false,
// Whether to show the map
'show_map' => true,
/**
* Set whether & where to display the address input.
*
* Available values:
* - before_map: Show it before the map
* - true/after_map: Show it after the map
* - false: Hide the address field
*/
'show_address' => 'after_map',
// The address value
'address' => '',
/**
* Markers
*/
// Show the markers list
'show_markers_list' => false,
// Max markers allowed
'max_markers' => 1
];
public function __construct($options = [])
{
$this->widget_options = array_merge($this->widget_options, $this->_widget_options);
parent::__construct($options);
$this->prepare();
}
private function prepare()
{
// We do not show the map
if (!$this->options['show_map'])
{
$this->options['css_class'] .= ' no-map';
}
// Hide "Clear" button if no markers exists
if (empty($this->options['markers']))
{
$this->options['css_class'] .= ' clear-is-hidden';
}
if ((!$this->options['pro'] && count($this->options['markers']) >= 1) || ($this->options['max_markers'] !== 0 && count($this->options['markers']) >= $this->options['max_markers']))
{
$this->options['css_class'] .= ' markers-limit-reached';
}
Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_ALL_MARKERS');
Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_MARKER');
Text::script('NR_ADD_MARKER');
Text::script('NR_EDIT_MARKER');
Text::script('NR_DELETE_MARKER');
Text::script('NR_UNKNOWN_LOCATION');
}
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
if ($this->options['show_map'])
{
parent::loadMedia();
HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.contextmenu.min.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/vendor/leaflet.contextmenu.min.js', ['relative' => true, 'version' => 'auto']);
}
HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapaddresseditorview.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/widgets/mapaddresseditorview.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Rating.php 0000644 00000003057 15235314576 0010127 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Uri\Uri;
/**
* The Rating Widget
*/
class Rating extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The SVG icon representing the rating icon. Available values: check, circle, flag, heart, smiley, square, star, thumbs_up
'icon' => 'star',
// The default value of the widget.
'value' => 0,
// How many stars to show?
'max_rating' => 5,
// Whether to show half ratings
'half_ratings' => false,
// The size of the rating icon in pixels.
'size' => 24,
// The color of the icon in the default state
'selected_color' => '#f6cc01',
// The color of the icon in the selected and hover state
'unselected_color' => '#bdbdbd'
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
$this->options['value'] = $this->options['value'] > $this->options['max_rating'] ? $this->options['max_rating'] : $this->options['value'];
$this->options['icon_url'] = Uri::root() . 'media/plg_system_nrframework/svg/rating/' . $this->options['icon'] . '.svg';
$this->options['max_rating'] = $this->options['half_ratings'] ? 2 * $this->options['max_rating'] : $this->options['max_rating'];
}
} Widgets/MapAddressEditor.php 0000644 00000007037 15235314576 0012077 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class MapAddressEditor extends Widget
{
/**
* Default latitude.
*
* @var string
*/
private $default_lat = '38.24921060739844';
/**
* Default longitude.
*
* @var string
*/
private $default_long = '25.314512745029823';
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The map coordinates.
* Format: latitude,longitude
*
* i.e. 36.891319,27.283480
*/
'value' => '0,0',
/**
* Set whether and where to show the map.
*
* Available values:
*
* false
* backend
* frontend
* both
*/
'show_map' => false,
// The actual map HTML
'map' => false,
// Whether autocomplete is enabled for the address field
'autocomplete' => false,
// Set what information the user can see/edit when selecting an address.
'showAddressDetails' => [
'address' => false,
'latitude' => false,
'longitude' => false,
'country' => false,
'country_code' => false,
'city' => false,
'postal_code' => false,
'county' => false,
'state' => false,
'municipality' => false,
'town' => false,
'road' => false,
],
/**
* The address details.
*
* Supported data:
*
* address
* latitude
* longitude
* country
* country_code
* city
* postal_code
* county
* state
* municipality
* town
* road
*/
'address' => [
'address' => '',
'latitude' => '',
'longitude' => '',
'country' => '',
'country_code' => '',
'city' => '',
'postal_code' => '',
'county' => '',
'state' => '',
'municipality' => '',
'town' => '',
'road' => '',
],
/**
* Map location in correlation with the address details.
*
* Note: This takes effect only if no custom layout is used.
*
* Available values:
*
* - above (Above the address details)
* - below (Below the address details)
*/
'map_location' => 'below'
];
public function __construct($options = [])
{
parent::__construct($options);
if (isset($options['_showAddressDetails']))
{
$this->options['showAddressDetails'] = array_merge($this->options['showAddressDetails'], $this->options['_showAddressDetails']);
}
if ($options['required'])
{
$this->options['css_class'] = ' is-required';
}
$this->options['enable_info_window'] = false;
}
/**
* Renders the widget
*
* @return string
*/
public function render()
{
$this->loadMedia();
$show_map = in_array($this->options['show_map'], ['backend', 'both']);
// Get the map editor
$map_options = array_merge($this->options, [
'show_map' => $show_map,
'autocomplete' => $this->options['autocomplete'],
'address' => isset($this->options['address']['address']) ? $this->options['address']['address'] : ''
]);
$map = new MapAddressEditorView($map_options);
$map->loadMedia();
$this->options['map'] = $map->render();
return parent::render();
}
/**
* Loads media files
*
* @return void
*/
private function loadMedia()
{
HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapaddresseditor.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/widgets/mapaddresseditor.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/SelfHostedVideo.php 0000644 00000003660 15235314576 0011732 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class SelfHostedVideo extends Video
{
/**
* Widget default options
*
* @var array
*/
protected $video_widget_options = [
/**
* Specify how the video should be loaded when the page loads.
*
* Allowed values:
* - metadata
* - auto
* - none
*/
'preload' => 'auto',
// Whether to mute the video
'mute' => false,
// Whether to display controls on the video
'controls' => true,
// Whether to loop the video
'loop' => false,
// Stores the given video details
'video' => ''
];
protected function prepare()
{
if (isset($this->options['value']) && !empty($this->options['value']))
{
$videos = \NRFramework\Helpers\File::getFileSources($this->options['value'], ['mp4', 'webm', 'ogg', 'mov']);
$this->options['video'] = is_array($videos) && isset($videos[0]) ? $videos[0] : false;
}
$atts = [
'data-video-id="' . $this->options['value'] . '"',
'data-video-type="selfhostedvideo"',
'data-video-mute="' . var_export($this->options['mute'], true) . '"',
'data-video-controls="' . var_export($this->options['controls'], true) . '"',
'data-video-loop="' . var_export($this->options['loop'], true) . '"',
'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
];
$this->options['atts'] = implode(' ', $atts);
}
/**
* Loads media files
*
* @return void
*/
public function videoAssets()
{
HTMLHelper::script('plg_system_nrframework/widgets/video/selfhostedvideo.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Signature.php 0000644 00000003407 15235314576 0010643 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
/**
* Signature
*/
class Signature extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The base64 image data of the signature.
'value' => '',
// The width of the signature in pixels or empty for auto width. The width will be taken from the signature container.
'width' => '',
// The height of the signature in pixels.
'height' => '300px',
// The background color of the signature.
'background_color' => '#ffffff',
// The border color of the canvas.
'border_color' => '#dedede',
/**
* The border radius of the canvas.
*
* Example values: 0, 0px, 50px, 50%
*/
'border_radius' => 0,
/**
* The border width of the canvas.
*
* Example values: 0, 1px, 5px
*/
'border_width' => '1px',
// Whether to show the horizontal line within the canvas
'show_line' => true,
/**
* The line color.
*
* If `null`, retrieves the value from `border_color`
*/
'line_color' => null,
// The pen color
'pen_color' => '#000'
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
if ($this->options['readonly'])
{
$this->options['css_class'] .= ' readonly';
}
if (!empty($this->options['value']))
{
$this->options['css_class'] .= ' painted has-value';
}
if ($this->options['show_line'])
{
$this->options['css_class'] .= ' show-line';
}
}
} Widgets/RangeSlider.php 0000644 00000003150 15235314576 0011074 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
/**
* The Range Slider widget
*/
class RangeSlider extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The default value of the widget.
'value' => 0,
// The minimum value of the slider
'min' => 0,
// The maximum value of the slider
'max' => 100,
// The step of the slider
'step' => 1,
// The main slider color
'color' => '#1976d2',
// The input border color of the slider inputs
'input_border_color' => '#bdbdbd',
// The input background color of the slider inputs
'input_bg_color' => 'transparent'
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
// Base color is 20% of given color
$this->options['base_color'] = $this->options['color'] . '33';
// Calculate value
$this->options['value'] = (float) $this->options['value'] < $this->options['min'] ? $this->options['min'] : ((float) $this->options['value'] > $this->options['max'] ? $this->options['max'] : (float) $this->options['value']);
// Calculate bar percentage
$this->options['bar_percentage'] = $this->options['max'] ? floor(100 * ($this->options['value'] - $this->options['min']) / ($this->options['max'] - $this->options['min'])) : $this->options['value'];
}
} Widgets/GoogleMap.php 0000644 00000001410 15235314576 0010544 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class GoogleMap extends Map
{
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
parent::loadMedia();
HTMLHelper::script('plg_system_nrframework/widgets/googlemap.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('https://maps.googleapis.com/maps/api/js?callback=Function.prototype&key=' . $this->options['provider_key'], ['relative' => false, 'version' => false]);
}
} Widgets/Helper.php 0000644 00000005341 15235314576 0010120 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\Filesystem\Folder;
class Helper
{
/**
* This is a map with all widgets used for caching the widget's class name
*
* @var array
*/
public static $widgets_map = [];
/**
* Renders a Widget and returns
*
* @param array $options A list of attributes passed to the layout
*
* @return string The widget's final HTML layout
*/
public static function render($widget_name, $options = [])
{
if (!$widgetClass = self::find($widget_name))
{
return;
}
$class = __NAMESPACE__ . '\\' . $widgetClass;
// ensure class exists
if (!class_exists($class))
{
return;
}
return (new $class($options))->render();
}
/**
* Return the real class name of a widget by a case-insensitive name.
*
* @param string $name The widget's name
*
* @return mixed Null when the class name is not found, string when the class name is found.
*/
public static function find($name)
{
if (!$name)
{
return;
}
$name = strtolower($name);
if (empty(self::$widgets_map) || !isset(self::$widgets_map[$name]))
{
$widgetClasses = Folder::files(__DIR__);
foreach ($widgetClasses as $widgetClass)
{
$widgetClass = str_replace('.php', '', $widgetClass);
self::$widgets_map[strtolower($widgetClass)] = $widgetClass;
}
}
return isset(self::$widgets_map[$name]) ? self::$widgets_map[$name] : null;
}
/**
* Returns all layout overrides of a widget by its name.
*
* @param string $name
*
* @return array
*/
public static function getLayoutOverrides($name = '')
{
if (!$name)
{
return;
}
$path = self::getLayoutOverridePath($name);
if (!is_dir($path))
{
return;
}
$labels = array_diff(scandir($path), ['.', '..', '.DS_Store']);
$values = array_map(function($value) {
return rtrim($value, '.php');
}, $labels);
return array_combine($values, $labels);
}
/**
* Returns the layout override path of a widget by its name.
*
* @param string $name
*
* @return string
*/
public static function getLayoutOverridePath($name = '')
{
if (!$name)
{
return;
}
return implode(DIRECTORY_SEPARATOR, [JPATH_SITE, 'templates', \NRFramework\Helpers\Template::getTemplateName(), 'html', 'tassos', 'widgets', $name]);
}
} Widgets/MapEditor.php 0000644 00000010541 15235314576 0010563 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;
class MapEditor extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The list of markers added to the map.
*
* Example:
*
* [
* 'lat' => 37.9838,
* 'lng' => 23.7275,
* 'title' => 'Athens',
* 'description' => 'The capital of Greece',
* ]
*/
'value' => [],
// The default map latitude. Where it points when no markers are added.
'lat' => null,
// The default map longitude. Where it points when no markers are added.
'lng' => null,
// Max markers allowed
'maxMarkers' => 1,
// Set whether to show the map editor sidebar
'showSidebar' => true,
// Set the marker image, relative path to an image file
'markerImage' => '',
// TODO: Remove this once ACF is updated and after a reasonable time
'hide_input' => false
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
$this->loadMedia();
}
private function prepare()
{
if (!$this->options['pro'] && is_array($this->options['value']) && count($this->options['value']) >= 1)
{
$this->options['css_class'] .= ' markers-limit-reached';
}
if ($this->options['markerImage'])
{
$markerImage = explode('#', ltrim($this->options['markerImage'], DIRECTORY_SEPARATOR));
$this->options['markerImage'] = Uri::root() . reset($markerImage);
}
Text::script('NR_ENTER_AN_ADDRESS_OR_COORDINATES');
Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_ALL_SELECTED_MARKERS');
Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_MARKER');
Text::script('NR_ADD_MARKER');
Text::script('NR_EDIT_MARKER');
Text::script('NR_DELETE_MARKER');
Text::script('NR_UNKNOWN_LOCATION');
Text::script('NR_UNLIMITED_MARKERS');
Text::script('NR_ADD_MORE_MARKERS_UPGRADE_TO_PRO');
Text::script('NR_MARKERS');
Text::script('NR_YOU_HAVENT_ADDED_ANY_MARKERS_YET');
Text::script('NR_ADD_YOUR_FIRST_MARKER');
Text::script('NR_NO_MARKERS_FOUND');
Text::script('NR_LOCATION_ADDRESS');
Text::script('NR_ADD_TO_MAP');
Text::script('NR_COORDINATES');
Text::script('NR_ADDRESS_ADDRESS_HINT');
Text::script('NR_LATITUDE');
Text::script('NR_LONGITUDE');
Text::script('NR_MARKER_INFO');
Text::script('NR_LABEL');
Text::script('NR_DESCRIPTION');
Text::script('NR_MARKER_LABEL');
Text::script('NR_MARKER_DESCRIPTION');
Text::script('NR_SAVE');
Text::script('NR_PLEASE_SELECT_A_LOCATION');
Text::script('NR_IMPORT');
Text::script('NR_IMPORT_MARKERS');
Text::script('NR_IMPORT_LOCATIONS_DESC');
Text::script('NR_IMPORT_LOCATIONS_DESC2');
Text::script('NR_PLEASE_ENTER_LOCATIONS_TO_IMPORT');
Text::script('NR_COULDNT_IMPORT_LOCATIONS');
Text::script('NR_ADDING_MARKERS');
Text::script('NR_SAVE_YOUR_FIRST_MARKER');
Text::script('NR_OUT_OF');
Text::script('NR_MARKERS_ADDED');
Text::script('NR_MARKERS_LIMIT_REACHED_DELETE_MARKER_TO_ADD');
Text::script('NR_EXPORT_MARKERS');
Text::script('NR_EXPORT_MARKERS_DESC');
Text::script('NR_THERE_ARE_NO_LOCATIONS_TO_EXPORT');
Text::script('NR_LOCATIONS_IMPORTED');
Factory::getDocument()->addScriptOptions('TFMapEditor', [
'images_url' => Uri::root() . 'media/plg_system_nrframework/css/vendor/images/',
]);
}
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
HTMLHelper::script('plg_system_nrframework/vendor/react.min.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/vendor/react-dom.min.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.min.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/vendor/leaflet.min.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapeditor.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/mapeditor.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/FAQ.php 0000644 00000017512 15235314576 0007313 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
class FAQ extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* FAQ Settings
*/
/**
* The questions and answers.
*
* Format:
*
* [
* [
* 'question' => 'Question 1',
* 'answer' => 'Answer 1'
* ],
* [
* 'question' => 'Question 2',
* 'answer' => 'Answer 2'
* ],
* ]
*/
'value' => '',
/**
* Requires "show_toggle_icon" to be enabled to work.
*
* Define the initial state of the FAQ.
*
* Available values:
*
* - first-open: Open the first question
* - all-open: Opens all questions
* - all-closed: Closes all questions
*/
'initial_state' => 'first-open',
// Set whether to have one question open at a time
'keep_one_question_open' => true,
// Set the columns.
'columns' => 1,
// Set the gap between the items.
'item_gap' => 16,
// Set the gap between the columns.
'column_gap' => 16,
// Set whether to display a separator between items
'separator' => false,
// Set the separator color
'separator_color' => '',
/**
* Item Settings
*/
// Each item background color.
'item_background_color' => null,
// Each item border radius.
'item_border_radius' => null,
// Each item padding.
'item_padding' => null,
/**
* Question
*/
// Question font size
'question_font_size' => null,
// Each question text color.
'question_text_color' => null,
/**
* Answer
*/
// Answer font size
'answer_font_size' => null,
// Each answer text color.
'answer_text_color' => null,
/**
* Icon Settings
*/
/**
* Whether to show an icon that can toggle the open/close state of the answer.
*
* If disabled, all answers will appear by default.
* If enabled, all answers will be hidden by default.
*/
'show_toggle_icon' => false,
/**
* Set the icon that will be used.
*
* Available values:
* - arrow
* - plus_minus
* - circle_arrow
* - circle_plus_minus
*/
'icon' => 'arrow',
/**
* Set the icon position.
*
* Available values:
*
* - right
* - left
*/
'icon_position' => 'right',
/**
* FAQ Schema
*/
// Set whether to generate the FAQ Schema on the page.
'generate_faq' => false,
// Custom Item CSS Classes
'item_css_class' => ''
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
/**
* Prepares the FAQ.
*
* @return void
*/
private function prepare()
{
if ($this->options['show_toggle_icon'])
{
$this->options['show_toggle_icon'] = true;
$this->options['css_class'] .= ' has-icons';
$this->options['css_class'] .= ' position-icon-' . $this->options['icon_position'];
$this->options['css_class'] .= ' has-icon-' . $this->options['icon'];
}
if (!empty($this->options['item_background_color']) && $this->options['item_background_color'] !== 'none')
{
$this->options['css_class'] .= ' has-item-bg-color';
}
if ($this->options['separator'])
{
$this->options['css_class'] .= ' has-separator';
}
$this->options['css_class'] .= ' ' . $this->options['initial_state'];
if ($this->options['keep_one_question_open'])
{
$this->options['css_class'] .= ' keep-one-question-open';
}
if ((int) $this->options['columns'] > 1)
{
$this->options['css_class'] .= ' has-columns';
}
$this->generateFAQ();
if ($this->options['load_css_vars'])
{
$this->options['custom_css'] = $this->getWidgetCSS();
}
}
private function generateFAQ()
{
// Ensure "generate_faq" is enabled
if (!$this->options['generate_faq'])
{
return;
}
// Ensure we have questions and answers
if (!is_array($this->options['value']) && !count($this->options['value']))
{
return;
}
// Abort if FAQ cannot be compiled
if (!$faq = $this->getFAQ())
{
return;
}
// Hook into GSD to add the FAQ
Factory::getApplication()->registerEvent('onGSDBeforeRender', function(&$data) use ($faq)
{
try
{
// get the data
$tmpData = $data;
if (defined('nrJ4'))
{
$tmpData = $data->getArgument('0');
}
// Append the FAQ Schema
$tmpData[] = $faq;
// Ensure unique FAQ
$tmpData = array_unique($tmpData);
// Set back the new value to $data object
if (defined('nrJ4'))
{
$data->setArgument(0, $tmpData);
}
else
{
$data = $tmpData;
}
} catch (\Throwable $th)
{
$this->throwError($th->getMessage());
}
});
}
/**
* Returns the FAQ JSON/LD code.
*
* @return string
*/
private function getFAQ()
{
$autoload_file = JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';
if (!file_exists($autoload_file))
{
return;
}
require_once $autoload_file;
// Prepare the FAQ
$payload = [
'mode' => 'manual',
'faq_repeater_fields' => json_decode(json_encode($this->options['value']))
];
$payload = new Registry($payload);
$faq = new \GSD\Schemas\Schemas\FAQ($payload);
// Get the JSON/LD code of the FAQ
$json = new \GSD\Json($faq->get());
// Return the code
return $json->generate();
}
/**
* Returns the CSS for the widget.
*
* @param array $exclude_breakpoints Define breakpoints to exclude their CSS
*
* @return string
*/
public function getWidgetCSS($exclude_breakpoints = [])
{
$controls = [
// CSS Variables
[
'property' => '--item-background-color',
'value' => $this->options['item_background_color']
],
[
'property' => '--question-text-color',
'value' => $this->options['question_text_color']
],
[
'property' => '--answer-text-color',
'value' => $this->options['answer_text_color']
],
[
'property' => '--separator-color',
'value' => $this->options['separator_color']
],
// CSS
[
'property' => '--item-padding',
'type' => 'Spacing',
'value' => $this->options['item_padding'],
'unit' => 'px'
],
[
'property' => '--item-gap',
'value' => $this->options['item_gap'],
'unit' => 'px'
],
[
'property' => '--column-gap',
'value' => $this->options['column_gap'],
'unit' => 'px'
],
[
'property' => '--item-border-radius',
'type' => 'Spacing',
'value' => $this->options['item_border_radius'],
'unit' => 'px'
],
[
'property' => '--question-font-size',
'value' => $this->options['question_font_size'],
'unit' => 'px'
],
[
'property' => '--answer-font-size',
'value' => $this->options['answer_font_size'],
'unit' => 'px'
],
];
$selector = '.tf-faq-widget.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
return $controlsCSS;
}
/**
* Returns all CSS files.
*
* @return array
*/
public static function getCSS()
{
return [
'plg_system_nrframework/widgets/faq.css'
];
}
/**
* Returns all JS files.
*
* @param string $theme
*
* @return array
*/
public static function getJS()
{
return [
'plg_system_nrframework/widgets/faq.js'
];
}
} Widgets/ColorPicker.php 0000644 00000001430 15235314576 0011110 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
/**
* Color picker
*/
class ColorPicker extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The default value of the widget.
'value' => '#dedede',
// The input border color
'input_border_color' => '#dedede',
// The input border color on focus
'input_border_color_focus' => '#dedede',
// The input background color
'input_bg_color' => '#fff',
// Input text color
'input_text_color' => '#333'
];
} Widgets/GalleryManager2.php 0000644 00000016537 15235314576 0011666 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Factory;
use NRFramework\Helpers\Widgets\GalleryManager2 as GalleryManagerHelper;
use NRFramework\Image;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
/**
* Gallery Manager
*/
class GalleryManager2 extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The uploaded images
'value' => [],
// The input name
'name' => '',
// Context of the field
// module, default
'context' => 'default',
// The field ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
'field_id' => null,
// The item ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
'item_id' => null,
/**
* Max file size in MB.
*
* Defults to 0 (no limit).
*/
'max_file_size' => 0,
/**
* How many files we can upload.
*
* Defaults to 0 (no limit).
*/
'limit_files' => 0,
// Allowed upload file types
'allowed_file_types' => 'image/*',
/**
* Original Image
*/
// Original image resize width
'original_image_resize_width' => null,
// Original image resize height
'original_image_resize_height' => null,
/**
* Thumbnails
*/
// Thumbnails width
'thumb_width' => null,
// Thumbnails height
'thumb_height' => null,
// Thumbnails resize method (crop, stretch, fit)
'thumb_resize_method' => 'crop',
// The list of tags already available for this gallery
'tags' => [],
// The widget name
'widget' => 'GalleryManager2'
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
// Load translation strings
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
}
private function prepare()
{
$this->includeTempFiles();
// Set css class for readonly state
if ($this->options['readonly'])
{
$this->options['css_class'] .= ' readonly';
}
// Adds a css class when the gallery contains at least one item
if (is_array($this->options['value']) && count($this->options['value']))
{
$this->options['css_class'] .= ' dz-has-items';
}
// Load translation strings
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
$this->prepareTags();
}
/**
* Find and include temp files in the gallery.
*
* @return void
*/
private function includeTempFiles()
{
$ds = DIRECTORY_SEPARATOR;
$tempFolder = GalleryManagerHelper::getFullTempFolder($this->options['context'], $this->options['field_id'], $this->options['item_id']);
if (!is_dir($tempFolder))
{
return;
}
$files = Folder::files($tempFolder, '.', false, false, ['.', '..', 'index.html', 'index.php']);
if (!$files)
{
return;
}
$relativeTempFolder = ltrim(str_replace(JPATH_ROOT, '', $tempFolder), $ds);
foreach ($files as $filename)
{
$this->options['value'][] = [
'source' => implode($ds, [$relativeTempFolder, $filename]),
'original' =>'',
'exists' => true,
'caption' => '',
'thumbnail' => '',
'slideshow' => '',
'alt' => '',
'tags' => json_encode([]),
'temp' => true
];
}
}
private function prepareTags()
{
if (!is_array($this->options['value']))
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title')])
->from($db->quoteName('#__tags'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('level') . ' > 0');
$db->setQuery($query);
$tags = $db->loadAssocList('id', 'title');
$this->options['tags'] = $tags;
}
/**
* The upload task called by the AJAX hanler
*
* @return void
*/
protected function ajax_upload()
{
// Increase memory size and execution time to prevent PHP errors on datasets > 20K
set_time_limit(300); // 5 Minutes
ini_set('memory_limit', '-1');
$input = Factory::getApplication()->input;
$random_suffix = $input->get('random_suffix', 'false') === 'true' ? true : false;
// Make sure we have a valid context
if (!$context = $input->get('context'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
}
// Make sure we have a valid file passed
if (!$file = $input->files->get('file'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
}
// In case we allow multiple uploads the file parameter is a 2 levels array.
$first_property = array_pop($file);
if (is_array($first_property))
{
$file = $first_property;
}
$uploadSettings = [
'context' => $context,
'field_id' => $input->getInt('field_id'),
'item_id' => $input->getInt('item_id'),
'allow_unsafe' => false,
'allowed_types' => $this->widget_options['allowed_file_types'],
'random_suffix' => $random_suffix
];
// Upload the file and resize the images as required
if (!$source = GalleryManagerHelper::upload($file, $uploadSettings))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_CANNOT_UPLOAD_FILE');
}
echo json_encode([
'source' => $source
]);
}
/**
* The delete task called by the AJAX hanlder
*
* @return void
*/
protected function ajax_delete()
{
// Increase memory size and execution time to prevent PHP errors on datasets > 20K
set_time_limit(300); // 5 Minutes
ini_set('memory_limit', '-1');
$input = Factory::getApplication()->input;
// Get source image path.
$source = $input->getString('source');
// Get the slideshow image path.
$slideshow = $input->getString('slideshow', '');
// Get the original image
$original = $input->getString('original');
// Get the thumbnail image
$thumbnail = $input->getString('thumbnail');
if (!$context = $input->get('context'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
}
$field_id = $input->getInt('field_id');
$item_id = $input->getInt('item_id');
if (!$field_data = GalleryManagerHelper::getSettings($context, $field_id, $item_id))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
// Delete the source, original, and thumbnail file
$deleted = GalleryManagerHelper::deleteFile($source, $slideshow, $original, $thumbnail);
echo json_encode(['success' => $deleted]);
}
/**
* Exits the page with given message.
*
* @param string $translation_string
*
* @return void
*/
private function exitWithMessage($translation_string)
{
http_response_code('500');
die(Text::_($translation_string));
}
} Widgets/Widget.php 0000644 00000010712 15235314576 0010122 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Session\Session;
class Widget
{
protected $widget_options = [];
/**
* Widget's default options
*
* @var array
*/
protected $options = [
// Set whether to load the CSS variables
'load_css_vars' => true,
// Set whether to load the default stylesheet
'load_stylesheet' => true,
// If true, the widget will be rended in read-only mode.
'readonly' => false,
// If true, the widget will be rended in disabled mode.
'disabled' => false,
// Indicates the widget's input field must be filled out before submitting the form.
'required' => false,
// The CSS class to be used on the widget's wrapper
'css_class' => '',
// The CSS class to be used on the input
'input_class' => '',
// The input name
'name' => '',
// The default widget value
'value' => '',
// Extra attributes
'atts' => '',
// Custom CSS
'custom_css' => '',
// A short hint that describes the expected value
'placeholder' => '',
// The name of the layout to be used to render the widget
'layout' => 'default',
// The extension name where the widget is loaded from.
'extension' => '',
// The aria-label attribute
'aria_label' => '',
// Whether we are rendering the Pro version of the widget
'pro' => false
];
/**
* If no name is provided, this counter is appended to the widget's name to prevent name conflicts
*
* @var int
*/
protected static $counter = 0;
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
// Merge Widget class default options with given Widget default options
$this->options = array_merge($this->options, $this->widget_options, $options);
// Set ID if none given
if (!isset($this->options['id']))
{
$this->options['id'] = $this->getName() . self::$counter;
}
// Help developers target the whole widget by applying the widget's ID to the CSS class list.
// Do not use the id="xx" attribute in the HTML to prevent conflicts with the input's ID.
$this->options['css_class'] .= ' ' . $this->options['id'];
// Set name if none given
if (!isset($this->options['name']))
{
$this->options['name'] = $this->options['id'];
}
// Set disabled class if widget is disabled
if ($this->options['disabled'])
{
$this->options['css_class'] .= ' disabled';
}
self::$counter++;
}
/**
* Renders the widget with the given layout
*
* Layouts can be overriden in the following folder: /templates/TEMPLATE_NAME/html/tassos/WIDGET_NAME/LAYOUT_NAME.php
*
* @return string
*/
public function render()
{
$defaultPath = implode(DIRECTORY_SEPARATOR, [JPATH_PLUGINS, 'system', 'nrframework', 'layouts']);
$overridePath = implode(DIRECTORY_SEPARATOR, [JPATH_THEMES, Factory::getApplication()->getTemplate(), 'html', 'tassos']);
$layout = new FileLayout('widgets.' . $this->getName() . '.' . $this->options['layout'], null, ['debug' => false]);
$layout->addIncludePaths($defaultPath);
$layout->addIncludePaths($overridePath);
return $layout->render($this->options);
}
/**
* Get the name of the widget
*
* @return void
*/
public function getName()
{
return strtolower((new \ReflectionClass($this))->getShortName());
}
/**
* Returns the options key value.
*
* @param string $key
*
* @return mixed
*/
public function getOption($key)
{
return isset($this->options[$key]) ? $this->options[$key] : null;
}
/**
* Manages ajax requests for the widget.
*
* @param string $task
*
* @return void
*/
public function onAjax($task)
{
Session::checkToken('request') or die('Invalid Token');
if (!$task || !is_string($task))
{
return;
}
$method = 'ajax_' . $task;
if (!method_exists($this, $method))
{
return;
}
$this->$method();
}
/**
* Sets the options key value.
*
* @param string $key
*
* @return void
*/
public function setOption($key, $value)
{
$this->options[$key] = $value;
}
/**
* Checks if an option exists.
*
* @param string $key
*
* @return bool
*/
public function optionExists($key)
{
return isset($this->options[$key]);
}
} Widgets/GalleryManager.php 0000644 00000043434 15235314576 0011600 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Factory;
use NRFramework\Helpers\Widgets\GalleryManager as GalleryManagerHelper;
use NRFramework\Image;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;
/**
* Gallery Manager
*/
class GalleryManager extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The input name
'name' => '',
// Context of the field
// module, default
'context' => 'default',
// The field ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
'field_id' => null,
// The item ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
'item_id' => null,
/**
* Max file size in MB.
*
* Defults to 0 (no limit).
*/
'max_file_size' => 0,
/**
* How many files we can upload.
*
* Defaults to 0 (no limit).
*/
'limit_files' => 0,
// Allowed upload file types
'allowed_file_types' => '.jpg, .jpeg, .png, .gif, .webp, image/webp',
/**
* Original Image
*/
// Original image resize width
'original_image_resize_width' => null,
// Original image resize height
'original_image_resize_height' => null,
/**
* Thumbnails
*/
// Thumbnails width
'thumb_width' => null,
// Thumbnails height
'thumb_height' => null,
// Thumbnails resize method (crop, stretch, fit)
'thumb_resize_method' => 'crop',
// The list of tags already available for this gallery
'tags' => [],
// The widget name
'widget' => 'GalleryManager'
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
// Load translation strings
Text::script('NR_GALLERY_MANAGER_CONFIRM_REGENERATE_IMAGES');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
}
private function prepare()
{
// Set gallery items
$this->options['gallery_items'] = is_array($this->options['value']) ? $this->options['value'] : [];
// Set css class for readonly state
if ($this->options['readonly'])
{
$this->options['css_class'] .= ' readonly';
}
// Adds a css class when the gallery contains at least one item
if (count($this->options['gallery_items']))
{
$this->options['css_class'] .= ' dz-has-items';
}
// Load translation strings
Text::script('NR_GALLERY_MANAGER_CONFIRM_REGENERATE_IMAGES');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
$this->prepareTags();
}
private function prepareTags()
{
if (!is_array($this->options['gallery_items']))
{
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title')])
->from($db->quoteName('#__tags'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('level') . ' > 0');
$db->setQuery($query);
$tags = $db->loadAssocList('id', 'title');
$this->options['tags'] = $tags;
}
private function getSettings($context)
{
// Make sure we have a valid context
if (!$context)
{
return false;
}
$field_data = [];
$input = Factory::getApplication()->input;
if ($context === 'default')
{
// Make sure we have a valid field id
if (!$field_id = $input->getInt('field_id'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_FIELD_ID_ERROR');
}
if (!$field_data = \NRFramework\Helpers\CustomField::getData($field_id))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
}
else if ($context === 'module')
{
// Make sure we have a valid item id
if (!$item_id = $input->getInt('item_id'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ITEM_ID_ERROR');
}
if (!$field_data = \NRFramework\Helpers\Module::getData($item_id))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
$field_data->set('style', $field_data->get('provider', 'grid'));
}
return $field_data;
}
/**
* The upload task called by the AJAX hanler
*
* @return void
*/
protected function ajax_upload()
{
$input = Factory::getApplication()->input;
// Make sure we have a valid context
if (!$context = $input->get('context'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
}
// Make sure we have a valid file passed
if (!$file = $input->files->get('file'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
}
if (!$field_data = $this->getSettings($context))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
// get the media uploader file data, values are passed when we upload a file using the Media Uploader
$media_uploader_file_data = [
'is_media_uploader_file' => $input->get('media_uploader', false) == '1',
'media_uploader_filename' => $input->getString('media_uploader_filename', '')
];
// In case we allow multiple uploads the file parameter is a 2 levels array.
$first_property = array_pop($file);
if (is_array($first_property))
{
$file = $first_property;
}
$style = $field_data->get('style', 'grid');
$uploadSettings = [
'allow_unsafe' => false,
'allowed_types' => $field_data->get('allowed_file_types', $this->widget_options['allowed_file_types']),
'style' => $style
];
// Add watermark
if ($field_data->get('watermark.type', 'disabled') !== 'disabled')
{
$uploadSettings['watermark'] = (array) $field_data->get('watermark', []);
$uploadSettings['watermark']['image'] = !empty($uploadSettings['watermark']['image']) ? explode('#', JPATH_SITE . DIRECTORY_SEPARATOR . $uploadSettings['watermark']['image'])[0] : null;
$uploadSettings['watermark']['apply_on_thumbnails'] = $field_data->get('watermark.apply_on_thumbnails', false) === '1';
}
$field_data_array = $field_data->toArray();
$resize_method = $field_data->get('resize_method', 'crop');
$thumb_height = $field_data->get('thumb_height', null);
switch ($style)
{
case 'slideshow':
if (isset($field_data_array['slideshow_thumb_height']))
{
$thumb_height = $field_data_array['slideshow_thumb_height'];
}
if ($slideshow_resize_method = $field_data->get('slideshow_resize_method'))
{
$resize_method = $slideshow_resize_method;
}
break;
case 'masonry':
$thumb_height = null;
break;
case 'zjustified':
case 'justified':
$thumb_height = $field_data->get('justified_item_height', 200);
break;
}
// resize image settings
$resizeSettings = [
'thumb_height' => $thumb_height,
'thumb_resize_method' => $resize_method,
// TODO: Remove this line when ACF is also updated, so we don't rely on this to resize the original image
'original_image_resize' => false,
'original_image_resize_width' => $field_data->get('original_image_resize_width'),
'original_image_resize_height' => $field_data->get('original_image_resize_height')
];
/**
* For backwards compatibility.
*
* TODO: Update this code block to not rely on "original_image_resize" to resize original image when removed from ACF.
*/
$resize_original_image_setting_value = $field_data->get('original_image_resize', null);
if ($style === 'slideshow' && ($resizeSettings['original_image_resize_width'] || $resizeSettings['original_image_resize_height']))
{
$resize_original_image_setting_value = true;
}
if ($resize_original_image_setting_value)
{
$resizeSettings['original_image_resize_height'] = $style === 'slideshow' ? $resizeSettings['original_image_resize_height'] : null;
$resizeSettings['original_image_resize'] = $style === 'slideshow' ? true : $resize_original_image_setting_value;
}
else if (is_null($resize_original_image_setting_value) && ($resizeSettings['original_image_resize_width'] || $resizeSettings['original_image_resize_height']))
{
$resizeSettings['original_image_resize'] = true;
}
if (!$resizeSettings['original_image_resize'])
{
$resizeSettings['original_image_resize_width'] = null;
$resizeSettings['original_image_resize_height'] = null;
}
if (in_array($style, ['grid', 'masonry', 'slideshow']))
{
$resizeSettings['thumb_width'] = $field_data->get('thumb_width');
$slideshow_thumb_width = $field_data->get('slideshow_thumb_width');
if (!is_null($slideshow_thumb_width) && $style === 'slideshow')
{
$resizeSettings['thumb_width'] = $slideshow_thumb_width;
}
}
// Upload the file and resize the images as required
if (!$uploaded_filenames = GalleryManagerHelper::upload($file, $uploadSettings, $media_uploader_file_data, $resizeSettings))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_CANNOT_UPLOAD_FILE');
}
echo json_encode([
'source' => $uploaded_filenames['source'],
'original' => $uploaded_filenames['original'],
'thumbnail' => $uploaded_filenames['thumbnail'],
'is_media_uploader_file' => $media_uploader_file_data['is_media_uploader_file']
]);
}
/**
* The delete task called by the AJAX hanlder
*
* @return void
*/
protected function ajax_delete()
{
$input = Factory::getApplication()->input;
// Get source image path.
$source = $input->getString('source');
// Make sure we have a valid file passed
if (!$original = $input->getString('original'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
}
// Make sure we have a valid file passed
if (!$thumbnail = $input->getString('thumbnail'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
}
if (!$context = $input->get('context'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
}
if (!$field_data = $this->getSettings($context))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
}
// Delete the source, original, and thumbnail file
$deleted = GalleryManagerHelper::deleteFile($source, $original, $thumbnail);
echo json_encode(['success' => $deleted]);
}
/**
* This task allows us to regenerate the images.
*
* @return void
*/
protected function ajax_regenerate_images()
{
$input = Factory::getApplication()->input;
// Make sure we have a valid context
if (!$context = $input->get('context'))
{
echo json_encode(['success' => false, 'message' => Text::_('NR_GALLERY_MANAGER_CONTEXT_ERROR')]);
die();
}
if (!$field_data = $this->getSettings($context))
{
echo json_encode(['success' => false, 'message' => Text::_('NR_GALLERY_MANAGER_INVALID_FIELD_DATA')]);
die();
}
$field_id = $input->getInt('field_id');
$item_id = $input->getInt('item_id');
$field_data_array = $field_data->toArray();
$style = $field_data->get('style', 'grid');
$resize_method = $field_data->get('resize_method', 'crop');
$thumb_height = $field_data->get('thumb_height', null);
switch ($style)
{
case 'slideshow':
if (isset($field_data_array['slideshow_thumb_height']))
{
$thumb_height = $field_data_array['slideshow_thumb_height'];
}
if ($slideshow_resize_method = $field_data->get('slideshow_resize_method'))
{
$resize_method = $slideshow_resize_method;
}
break;
case 'masonry':
$thumb_height = null;
break;
case 'zjustified':
case 'justified':
$thumb_height = $field_data->get('justified_item_height', 200);
break;
}
$resizeSettings = [
'thumb_height' => $thumb_height,
'thumb_resize_method' => $resize_method
];
if (in_array($style, ['grid', 'masonry', 'slideshow']))
{
$resizeSettings['thumb_width'] = $field_data->get('thumb_width');
$slideshow_thumb_width = $field_data->get('slideshow_thumb_width');
if (!is_null($slideshow_thumb_width) && $style === 'slideshow')
{
$resizeSettings['thumb_width'] = $slideshow_thumb_width;
}
}
// TODO: Remove this line when ACF is also updated, so we don't rely on this to resize the original image
$original_image_resize = false;
$original_image_resize_width = $field_data->get('original_image_resize_width');
$original_image_resize_height = $field_data->get('original_image_resize_height');
/**
* For backwards compatibility.
*
* TODO: Update this code block to not rely on "original_image_resize" to resize original image when removed from ACF.
*/
$resize_original_image_setting_value = $field_data->get('original_image_resize', null);
if ($style === 'slideshow' && ($original_image_resize_width || $original_image_resize_height))
{
$resize_original_image_setting_value = true;
}
if ($resize_original_image_setting_value)
{
$original_image_resize_height = $style === 'slideshow' ? $original_image_resize_height : null;
$original_image_resize = $style === 'slideshow' ? true : $resize_original_image_setting_value;
}
else if (is_null($resize_original_image_setting_value) && ($original_image_resize_width || $original_image_resize_height))
{
$original_image_resize = true;
}
if (!$original_image_resize)
{
$original_image_resize_width = null;
$original_image_resize_height = null;
}
$watermarkSettings = [];
// Add watermark
if ($field_data->get('watermark.type', 'disabled') !== 'disabled')
{
$watermarkSettings = (array) $field_data->get('watermark', []);
$watermarkSettings['image'] = !empty($watermarkSettings['image']) ? explode('#', JPATH_SITE . DIRECTORY_SEPARATOR . $watermarkSettings['image'])[0] : null;
$watermarkSettings['apply_on_thumbnails'] = $field_data->get('watermark.apply_on_thumbnails', false) === '1';
}
$watermarkEnabled = isset($watermarkSettings['type']) && $watermarkSettings['type'] !== 'disabled';
$thumbnailWatermarkEnabled = isset($watermarkSettings['type']) && $watermarkSettings['type'] !== 'disabled' && $watermarkSettings['apply_on_thumbnails'];
$items = $input->get('items', null, 'ARRAY');
$items = json_decode($items[0], true);
$ds = DIRECTORY_SEPARATOR;
// Parse all images
if (is_array($items) && count($items))
{
foreach ($items as &$item)
{
$sourceImage = isset($item['source']) ? $item['source'] : '';
$originalImage = isset($item['original']) ? $item['original'] : '';
$thumbnailImage = isset($item['thumbnail']) ? $item['thumbnail'] : '';
$thumbnailImagePath = implode($ds, [JPATH_ROOT, $thumbnailImage]);
$sourceImagePath = $sourceImage ? implode($ds, [JPATH_ROOT, $sourceImage]) : false;
$sourceImageExists = $sourceImagePath && file_exists($sourceImagePath);
$originalImagePath = implode($ds, [JPATH_ROOT, $originalImage]);
$originalImageExists = $originalImagePath && file_exists($originalImagePath);
// If source image does not exist, watermark is enabled, create it by clothing the original image
if (!$sourceImageExists && $watermarkEnabled && $originalImage && file_exists($originalImagePath))
{
// Create source from original image
$sourceImagePath = \NRFramework\File::copy($originalImagePath, $originalImagePath, false, true);
$sourceImageExists = true;
// Modify the database entry and add "source" image to item
// We just need the relative path to file
$_sourceImagePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $sourceImagePath);
$item['source'] = $_sourceImagePath;
$_originalImagePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $originalImagePath);
GalleryManagerHelper::setItemFieldSource($item_id, $field_id, $_sourceImagePath, $_originalImagePath);
}
if (!$originalImageExists)
{
continue;
}
if (!$sourceImageExists)
{
$sourceImagePath = $originalImagePath;
}
/**
* Handle original image.
*/
// Generate original image by using the source image
if ($original_image_resize_width && $original_image_resize_height)
{
$originalImagePath = Image::resize($sourceImagePath, $original_image_resize_width, $original_image_resize_height, 70, 'crop', $originalImagePath);
}
else if ($original_image_resize_width)
{
$originalImagePath = Image::resizeAndKeepAspectRatio($sourceImagePath, $original_image_resize_width, 70, $originalImagePath);
}
else if ($original_image_resize_height)
{
$originalImagePath = Image::resizeByHeight($sourceImagePath, $original_image_resize_height, $originalImagePath, 70);
}
$originalImageSourcePath = $originalImagePath;
if ($watermarkEnabled)
{
$payload = array_merge($watermarkSettings, ['source' => $sourceImagePath, 'destination' => $originalImagePath]);
Image::applyWatermark($payload);
}
/**
* Handle thumbnail image.
*/
// Generate thumbnail image by using the source image
GalleryManagerHelper::generateThumbnail($sourceImagePath, $thumbnailImagePath, $resizeSettings, null, false);
// Apply watermark to thumbnail image
if ($watermarkEnabled && $thumbnailWatermarkEnabled)
{
$payload = array_merge($watermarkSettings, ['source' => $thumbnailImagePath]);
Image::applyWatermark($payload);
}
}
}
echo json_encode(['success' => true, 'message' => Text::_('NR_GALLERY_MANAGER_IMAGES_REGENERATED'), 'items' => $items]);
}
/**
* Exits the page with given message.
*
* @param string $translation_string
*
* @return void
*/
private function exitWithMessage($translation_string)
{
http_response_code('500');
die(Text::_($translation_string));
}
} Widgets/MapAddress.php 0000644 00000010074 15235314576 0010723 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
class MapAddress extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The map coordinates.
* Format: latitude,longitude
*
* i.e. 36.891319,27.283480
*/
'value' => '',
/**
* Set whether and where to show the map.
*
* Available values:
*
* false
* backend
* frontend
* both
*/
'show_map' => false,
// The map HTML (If can be rendered)
'map' => false,
// Set what information the user can see.
'showAddressDetails' => [
'address' => true,
'latitude' => false,
'longitude' => false,
'country' => true,
'country_code' => false,
'city' => false,
'postal_code' => true,
'county' => false,
'state' => false,
'municipality' => false,
'town' => false,
'road' => false,
],
/**
* The address details.
*
* Supported data:
*
* address
* latitude
* longitude
* country
* country_code
* city
* postal_code
* county
* state
* municipality
* town
* road
*/
'address' => [
'address' => '',
'latitude' => '',
'longitude' => '',
'country' => '',
'country_code' => '',
'city' => '',
'postal_code' => '',
'county' => '',
'state' => '',
'municipality' => '',
'town' => '',
'road' => '',
],
/**
* The layout type of the output.
*
* Available values:
*
* - default
* - custom
*/
'layout_type' => 'default',
/**
* The custom layout code (HTML + Smart Tags).
*
* Available Smart Tags:
*
* Allowed Smart Tags:
*
* {address.map}
* {address.address} - {address.address.label}
* {address.latitude} - {address.latitude.label}
* {address.longitude} - {address.longitude.label}
* {address.country} - {address.country.label}
* {address.country_code} - {address.country_code.label}
* {address.city} - {address.city.label}
* {address.county} - {address.county.label}
* {address.postal_code} - {address.postal_code.label}
* {address.state} - {address.state.label}
* {address.municipality} - {address.municipality.label}
* {address.town} - {address.town.label}
* {address.road} - {address.road.label}
*/
'custom_layout' => '{address.address.label}: {address.address}',
/**
* Map location in correlation with the address details.
*
* Note: This takes effect only if no custom layout is used.
*
* Available values:
*
* - above (Above the address details)
* - below (Below the address details)
*/
'map_location' => 'below',
// The map HTML which will return the map HTML only if a map is set and current layout is not custom
'map_html' => ''
];
/**
* Renders the widget
*
* @return string
*/
public function render()
{
$this->options['enable_info_window'] = false;
if (in_array($this->options['show_map'], ['frontend', 'both']) || (in_array($this->options['show_map'], ['frontend', 'both']) && $this->options['layout_type'] === 'custom' && !empty($this->options['custom_layout']) && strpos($this->options['custom_layout'], '{address.map}') !== false))
{
$this->options['markerImage'] = $this->options['marker_image'];
// Get the map
$map_options = $this->options;
// Remove unneeded props.
// The custom layout can include Smart Tags and this breaks the Smart Tags replace method as it tries to replace inner Smart Tags too
unset($map_options['custom_layout']);
unset($map_options['showAddressDetails']);
unset($map_options['address']);
$map = new OpenStreetMap($map_options);
$map->loadMedia();
$this->options['map'] = $map->render();
}
$this->options['map_html'] = in_array($this->options['show_map'], ['frontend', 'both']) && $this->options['layout_type'] !== 'custom' ? $this->options['map'] : '';
return parent::render();
}
} Widgets/Accordion.php 0000644 00000021555 15235314576 0010607 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
use Joomla\CMS\HTML\HTMLHelper;
class Accordion extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* Accordion Settings
*/
/**
* The titles and contents of the accordion items.
*
* Format:
*
* [
* [
* 'title' => 'Title 1',
* 'content' => 'Content 1'
* ],
* [
* 'title' => 'Title 2',
* 'content' => 'Content 2'
* ],
* ]
*/
'value' => '',
/**
* Choose how spacious or compact you'd like to list accordion.
* Consider this the padding of each accordion item.
*
* Available values:
* - none
* - default
* - comfortable
* - compact
*/
'density' => 'default',
// Set the font size.
'font_size' => '16px',
/**
* Set the gap between the items.
*
* Available values:
* - none
* - small
* - large
*/
'gap' => 'none',
// Set the background color of the panel.
'panel_background_color' => '#fff',
// Set the color of the text and the icon.
'text_color' => '#333',
// Set the color of the 1px border that affects both item and container. Set to 'none' for no border color.
'border_color' => '#ddd',
/**
* Set the rounded corners of the items.
*
* Available values:
* - none
* - small
* - large
*/
'rounded_corners' => 'small',
/**
* Item Icon Settings
*/
/**
* Set whether to display a toggle icon next to the title, or not.
*
* Available values:
* - none
* - left
* - right
*/
'show_icon' => 'left',
/**
* Set the icon URL.
*/
'icon' => '',
/**
* Behavior
*/
/**
* Define the initial state of the accordion.
* By default all items are initially shown collapsed.
*
* Available values:
*
* - collapsed: All panels appear as collapsed.
* - expanded: All panels appear as expanded.
* - expanded-first: Expand the first panel only.
*/
'initial_state' => 'collapsed',
// Set whether to allow only one panel to be expanded at a time.
'only_one_panel_expanded' => false,
// Set whether to generate the FAQ Schema on the page.
'generate_faq' => false,
// Custom Panel CSS Class
'panel_css_class' => ''
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
/**
* Prepares the FAQ.
*
* @return void
*/
private function prepare()
{
$this->validateValue();
$this->generateFAQ();
// Set density
switch ($this->options['density'])
{
case 'none':
$this->options['density'] = 0;
break;
case 'default':
$this->options['density'] = '1em 1.25em';
break;
case 'comfortable':
$this->options['density'] = '1.25em 1.75em';
break;
case 'compact':
$this->options['density'] = '.65em 1.25em';
break;
}
// Set gap
switch ($this->options['gap'])
{
case 'none':
$this->options['gap'] = 0;
break;
case 'small':
$this->options['gap'] = '.3em';
break;
case 'large':
$this->options['gap'] = '.7em';
break;
}
// Set rounded corners
switch ($this->options['rounded_corners'])
{
case 'none':
$this->options['rounded_corners'] = 0;
break;
case 'small':
$this->options['rounded_corners'] = '.3em';
break;
case 'large':
$this->options['rounded_corners'] = '.7em';
break;
}
if ($this->options['only_one_panel_expanded'])
{
$this->options['css_class'] .= ' only-one-panel-expanded';
}
if ($this->options['load_css_vars'])
{
$this->options['custom_css'] = $this->getWidgetCSS();
}
}
/**
* Validates the value.
*
* @return void
*/
private function validateValue()
{
if (!is_array($this->options['value']))
{
return;
}
$st = new \NRFramework\SmartTags\SmartTags();
foreach ($this->options['value'] as $key => &$val)
{
if ((isset($val['title']) && empty($val['title'])) && (isset($val['content'])) && empty($val['content']))
{
unset($this->options['value'][$key]);
}
if ($this->options['pro'])
{
$val['title'] = HTMLHelper::_('content.prepare', $val['title']);
$val['content'] = HTMLHelper::_('content.prepare', $val['content']);
}
}
}
/**
* Generates the FAQ.
*
* @return void
*/
private function generateFAQ()
{
// Ensure "generate_faq" is enabled
if (!$this->options['generate_faq'])
{
return;
}
// Ensure we have value
if (!is_array($this->options['value']) && !count($this->options['value']))
{
return;
}
// Abort if FAQ cannot be compiled
if (!$faq = $this->getFAQ())
{
return;
}
// Hook into GSD to add the FAQ
Factory::getApplication()->registerEvent('onGSDBeforeRender', function(&$data) use ($faq)
{
try
{
// get the data
$tmpData = $data;
if (defined('nrJ4'))
{
$tmpData = $data->getArgument('0');
}
// Append the FAQ Schema
$tmpData[] = $faq;
// Ensure unique FAQ
$tmpData = array_unique($tmpData);
// Set back the new value to $data object
if (defined('nrJ4'))
{
$data->setArgument(0, $tmpData);
}
else
{
$data = $tmpData;
}
} catch (\Throwable $th)
{
$this->throwError($th->getMessage());
}
});
}
/**
* Returns the FAQ JSON/LD code.
*
* @return string
*/
private function getFAQ()
{
$autoload_file = JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';
if (!file_exists($autoload_file))
{
return;
}
require_once $autoload_file;
$value = $this->options['value'];
if (is_array($value))
{
foreach ($value as $key => &$val)
{
if (isset($val['title']))
{
$val['question'] = $val['title'];
unset($val['title']);
}
if (isset($val['content']))
{
$val['answer'] = $val['content'];
unset($val['content']);
}
}
}
// Prepare the FAQ
$payload = [
'mode' => 'manual',
'faq_repeater_fields' => json_decode(json_encode($value))
];
$payload = new Registry($payload);
$faq = new \GSD\Schemas\Schemas\FAQ($payload);
// Get the JSON/LD code of the FAQ
$json = new \GSD\Json($faq->get());
// Return the code
return $json->generate();
}
/**
* Returns the CSS for the widget.
*
* @param array $exclude_breakpoints Define breakpoints to exclude their CSS
*
* @return string
*/
public function getWidgetCSS($exclude_breakpoints = [])
{
$border_color = $this->options['border_color'] !== 'none' ? $this->options['border_color'] : 'transparent';
$controls = [];
// If no density is set, set the padding-top to 5px
if (!$this->options['density'])
{
$controls[] = [
'property' => '--content-padding-top',
'value' => '5px'
];
}
if (!$this->options['gap'])
{
$controls[] = [
'property' => '--container-border-color',
'value' => $border_color
];
$this->options['css_class'] .= ' no-gap';
}
$controls = array_merge($controls, [
// CSS Variables
[
'property' => '--panel-background-color',
'value' => $this->options['panel_background_color']
],
[
'property' => '--text-color',
'value' => $this->options['text_color']
],
[
'property' => '--panel-border-color',
'value' => $border_color
],
// CSS
[
'property' => '--padding',
'type' => 'Spacing',
'value' => $this->options['density']
],
[
'property' => '--gap',
'value' => $this->options['gap'],
'unit' => 'px'
],
[
'property' => '--rounded-corners',
'type' => 'Spacing',
'value' => $this->options['rounded_corners'],
'unit' => 'px'
],
[
'property' => '--font-size',
'value' => $this->options['font_size'],
'unit' => 'px'
]
]);
$selector = '.tf-accordion-widget.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
return $controlsCSS;
}
/**
* Returns all CSS files.
*
* @return array
*/
public static function getCSS()
{
return [
'plg_system_nrframework/widgets/accordion.css'
];
}
/**
* Returns all JS files.
*
* @param string $theme
*
* @return array
*/
public static function getJS()
{
return [
'plg_system_nrframework/widgets/accordion.js'
];
}
} Widgets/YouTube.php 0000644 00000007723 15235314576 0010303 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
class YouTube extends Video
{
/**
* Widget default options
*
* @var array
*/
protected $video_widget_options = [
/**
* Set the cover image type.
*
* Allowed Values:
* - none
* - auto
* - custom
*/
'coverImageType' => 'none',
// The Cover Image URL when coverImage="custom"
'coverImage' => '',
// Whether we allow fullscreen
'fs' => false,
// Whether controls will appear in the video
'controls' => true,
// Loop
'loop' => false,
// Mute
'mute' => false,
// Closed Captions
'cc_load_policy' => false,
/**
* The color that will be used in the player's video progress bar to highlight
* the amount of the video that the viewer has already seen.
*
* Allowed Values:
* - red
* - white
*/
'color' => 'red',
// Whether to allow or not keyboard shortcuts
'disablekb' => false,
// Start the video from X seconds
'start' => null,
// End the video at X seconds
'end' => null,
/**
* Set whether to show related videos.
*
* Allowed Values:
* 0: Don't show related videos
* 1: Show related videos from anywhere
*/
'rel' => '1',
/**
* Set whether to load the video in privacy-enhanced mode.
*
* When this is enabled, YouTube won't store information about
* visitors unless they play the video.
*/
'privacy' => false
];
/**
* Prepares the widget.
*
* @return void
*/
protected function prepare()
{
$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
// Abort
if ($videoProvider !== 'youtube')
{
$this->options['value'] = null;
return;
}
$this->options['css_class'] .= ' youtube';
$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';
if ($this->options['coverImageType'] === 'auto')
{
$this->options['coverImage'] = 'url("https://img.youtube.com/vi/' . $videoID . '/maxresdefault.jpg")';
}
else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
{
$coverImage = explode('#', $this->options['coverImage']);
$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
}
$atts = [
'data-video-id="' . $videoID . '"',
'data-video-controls="' . var_export($this->options['controls'], true) . '"',
'data-video-type="' . $videoProvider . '"',
'data-video-mute="' . var_export($this->options['mute'], true) . '"',
'data-video-loop="' . var_export($this->options['loop'], true) . '"',
'data-video-start="' . $this->options['start'] . '"',
'data-video-end="' . $this->options['end'] . '"',
'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
'data-video-fs="' . var_export($this->options['fs'], true) . '"',
'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
'data-video-cc="' . var_export($this->options['cc_load_policy'], true) . '"',
'data-video-disablekb="' . var_export($this->options['disablekb'], true) . '"',
'data-video-privacy="' . var_export($this->options['privacy'], true) . '"',
'data-video-rel="' . $this->options['rel'] . '"',
'data-video-color="' . $this->options['color'] . '"'
];
$this->options['atts'] = implode(' ', $atts);
}
/**
* We use the video widget layout file.
*
* @return string
*/
public function getName()
{
return 'video';
}
/**
* Loads media files
*
* @return void
*/
public function videoAssets()
{
HTMLHelper::script('plg_system_nrframework/widgets/video/youtube.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Slideshow.php 0000644 00000014271 15235314576 0010644 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
use NRFramework\Mimes;
use NRFramework\File;
use NRFramework\Image;
use Joomla\CMS\Factory;
class Slideshow extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// Slides per view
'slides_per_view' => [
'desktop' => 1
],
// Space between slides in px
'space_between_slides' => [
'desktop' => 10
],
// Enable Infinite Loop
'infinite_loop' => false,
// Enable Keyboard Control
'keyboard_control' => false,
/**
* Set the ordering.
*
* Available values:
* - default
* - alphabetical
* - reverse_alphabetical
* - random
*/
'ordering' => 'default',
/**
* The transition effect.
*
* Available values:
* - slide
* - fade
* - cube
* - coverflow
* - flip
*/
'transition_effect' => 'slide',
// Enable Autoplay
'autoplay' => false,
// Autoplay delay
'autoplay_delay' => 3000,
// Enable autoplay circular progress
'autoplay_progress' => false,
// Show thumbnails below the slideshow
'show_thumbnails' => false,
// Set whether to show arrows in the thumbnails slider
'show_thumbnails_arrows' => false,
/**
* Navigation controls.
*
* Accepted values:
* - arrows
* - dots
* - arrows_dots
*/
'nav_controls' => false,
// Theme color
'theme_color' => '#007aff',
// Set whether to display a lightbox
'lightbox' => false,
// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
'module' => '',
// Options
'options' => []
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
$this->prepareItems();
$this->setOrdering();
$this->setCSSVars();
}
private function prepare()
{
if ($this->options['lightbox'])
{
$this->options['css_class'] .= ' lightbox';
}
$options = [
'transition_effect' => $this->options['transition_effect'],
'infinite_loop' => $this->options['infinite_loop'],
'keyboard_control' => $this->options['keyboard_control'],
'autoplay' => $this->options['autoplay'],
'autoplay_delay' => $this->options['autoplay_delay'],
'autoplay_progress' => $this->options['autoplay_progress'],
'show_thumbnails' => $this->options['show_thumbnails'],
'show_thumbnails_arrows' => $this->options['show_thumbnails_arrows'],
'lightbox' => $this->options['lightbox'],
'breakpoints' => \NRFramework\Helpers\Responsive::getBreakpointsSettings(),
'slides_per_view' => $this->options['slides_per_view'],
'space_between_slides' => $this->getSpaceBetweenSlides(),
'nav_controls' => $this->options['nav_controls']
];
$this->options['options'] = $options;
}
private function getSpaceBetweenSlides()
{
$space_between_slides = $this->options['space_between_slides'];
if (is_array($space_between_slides))
{
foreach ($space_between_slides as $key => &$value)
{
$value = \NRFramework\Helpers\Controls\Control::getCSSValue($value['value']);
}
}
return $space_between_slides;
}
/**
* Prepares the items.
*
* - Sets the thumbnails image dimensions.
* - Assures caption property exist.
*
* @return mixed
*/
private function prepareItems()
{
if (!is_array($this->options['items']) || !count($this->options['items']))
{
return;
}
foreach ($this->options['items'] as $key => &$item)
{
// Initialize image atts
$item['img_atts'] = '';
// Initializes caption if none given
if (!isset($item['caption']))
{
$item['caption'] = '';
}
$item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
// Ensure a thumbnail is given
if (!isset($item['thumbnail_url']))
{
// If no thumbnail is given, set it to the full image
$item['thumbnail_url'] = $item['url'];
continue;
}
// If the thumbnail size for this item is given, set the image attributes
if (isset($item['thumbnail_size']))
{
$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
continue;
}
}
}
/**
* Sets the ordering of the gallery.
*
* @return void
*/
private function setOrdering()
{
switch ($this->options['ordering']) {
case 'random':
shuffle($this->options['items']);
break;
case 'alphabetical':
usort($this->options['items'], [$this, 'compareByThumbnailASC']);
break;
case 'reverse_alphabetical':
usort($this->options['items'], [$this, 'compareByThumbnailDESC']);
break;
}
}
/**
* Compares thumbnail file names in ASC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
private function compareByThumbnailASC($a, $b)
{
return strcmp(basename($a['thumbnail']), basename($b['thumbnail']));
}
/**
* Compares thumbnail file names in DESC order
*
* @param array $a
* @param array $b
*
* @return bool
*/
private function compareByThumbnailDESC($a, $b)
{
return strcmp(basename($b['thumbnail']), basename($a['thumbnail']));
}
/**
* Sets the CSS variables.
*
* @return void
*/
private function setCSSVars()
{
if (!$this->options['load_css_vars'])
{
return;
}
$controls = [
[
'property' => '--slideshow-slides-per-view',
'value' => $this->options['slides_per_view']
],
[
'property' => '--slideshow-space-between-slides',
'value' => $this->options['space_between_slides']
]
];
$selector = '.nrf-widget.tf-slideshow-wrapper.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
Factory::getDocument()->addStyleDeclaration($controlsCSS);
}
} Widgets/Map.php 0000644 00000014016 15235314576 0007415 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
abstract class Map extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The value of the widget.
* Format: latitude,longitude
*
* i.e. 36.891319,27.283480
*
* Otherwise, set the markers property
*/
'value' => '',
// Map tile provider key (if needed) to use the provider tiles
'provider_key' => null,
// Default map width
'width' => 500,
// Default map height
'height' => 400,
/**
* The Zoom Level.
*
* - preset: Set a fixed zoom.
* - fitbounds: Allow the map provider to auto-zoom and center the map around the markers.
*/
'zoom_level' => 'preset',
// Default map zoon
'zoom' => 4,
// Define lat,long format which will be used to center the map when zoom_level=preset is used.
'map_center' => null,
// Map scale. Values: metric, imperial, false
'scale' => false,
// View mode of the map.
'view' => '',
/**
* Set whether to show or not the map marker info window.
*
* Defaults to the map marker address (if not empty).
* If a map makrer label and/or description is set, these will be used.
*/
'enable_info_window' => true,
/**
* Map Marker
*/
/**
* The markers.
*
* An array of markers.
*
* [
* [
* latitude: 36.891319,
* longitude: 27.283480,
* label: 'Marker label',
* description: 'Marker description'
* ]
* ]
*/
'markers' => [],
// Marker image relative to Joomla installation
'markerImage' => ''
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
private function prepare()
{
$this->options['markerImage'] = $this->options['markerImage'] ? Uri::root() . ltrim($this->options['markerImage'], DIRECTORY_SEPARATOR) : '';
// Set the marker if a single value was given
if ($this->options['value'] && empty($this->options['markers']))
{
$coords = array_filter(array_map('trim', explode(',', $this->options['value'])));
if (count($coords) === 2)
{
$this->options['markers'] = [
[
'id' => 1,
'latitude' => $coords[0],
'longitude' => $coords[1]
]
];
}
}
// Make markers an array if a JSON string was given
if (is_string($this->options['markers']))
{
$this->options['markers'] = json_decode($this->options['markers'], true);
}
// Set as value the first marker so the JS library can have an initial center of the map
if (is_array($this->options['markers']))
{
$latitude = isset($this->options['markers'][0]['latitude']) ? $this->options['markers'][0]['latitude'] : false;
$longitude = isset($this->options['markers'][0]['longitude']) ? $this->options['markers'][0]['longitude'] : false;
if ($latitude && $longitude)
{
$this->options['value'] = implode(',', [$latitude, $longitude]);
}
}
if ($this->options['load_css_vars'])
{
$this->options['custom_css'] = $this->getWidgetCSS();
}
// Transform title/description translation strings
$this->prepareMarkerText();
}
private function prepareMarkerText()
{
if (!is_array($this->options['markers']) || !count($this->options['markers']))
{
return;
}
foreach ($this->options['markers'] as &$marker)
{
if (isset($marker['label']) && $marker['label'])
{
$marker['label'] = Text::_($marker['label']);
}
if (isset($marker['description']) && $marker['description'])
{
$marker['description'] = Text::_($marker['description']);
}
if (empty($marker['label']) && isset($marker['address']))
{
$marker['label'] = $marker['address'];
}
// Link to the item
if (!empty($marker['label']) && isset($marker['item_id']) && $marker['item_id'] && isset($marker['context']) && $marker['context'] && $marker['context'] !== 'com_users.user')
{
$context = explode('.', $marker['context']);
$routerHelper = $routerMethod = null;
// Content
if ($marker['context'] === 'com_content.article')
{
$routerHelper = defined('nrJ4') ? 'Joomla\Component\Content\Site\Helper\RouteHelper' : 'ContentHelperRoute';
$routerMethod = 'getArticleRoute';
}
// Contact
else
{
$routerHelper = defined('nrJ4') ? 'Joomla\Component\Contact\Site\Helper\RouteHelper' : 'ContactHelperRoute';
$routerMethod = 'getContactRoute';
}
$url = Route::_($routerHelper::$routerMethod($marker['item_id'], $marker['cat_id'], $marker['language']));
$marker['label'] = '<a href="' . $url . '">' . $marker['label'] . '</a>';
}
}
}
/**
* Returns the CSS for the widget.
*
* @param array $exclude_breakpoints Define breakpoints to exclude their CSS
*
* @return string
*/
public function getWidgetCSS($exclude_breakpoints = [])
{
$controls = [
// CSS Variables
[
'property' => '--width',
'value' => $this->options['width'],
'unit' => 'px'
],
[
'property' => '--height',
'value' => $this->options['height'],
'unit' => 'px'
],
];
$selector = '.nrf-widget.map-widget.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
return $controlsCSS;
}
public function render()
{
$this->loadMedia();
return parent::render();
}
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
if ($this->options['load_stylesheet'])
{
HTMLHelper::stylesheet('plg_system_nrframework/widgets/map.css', ['relative' => true, 'version' => 'auto']);
}
}
} Widgets/Countdown.php 0000644 00000032721 15235314576 0010663 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
/**
* Countdown
*/
class Countdown extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The Countdown type:
*
* - static: Counts down to a specific date and time. Universal deadline for all visitors.
* - evergreen: Set-and-forget solution. The countdown starts when your visitor sees the offer.
*/
'countdown_type' => 'static',
// The Static Countdown Date
'value' => '',
/**
* The timezone that will be used.
*
* - server - Use server's timezone
* - client - Use client's timezone
*/
'timezone' => 'server',
// Dynamic Days
'dynamic_days' => 0,
// Dynamic Hours
'dynamic_hours' => 0,
// Dynamic Minutes
'dynamic_minutes' => 0,
// Dynamic Seconds
'dynamic_seconds' => 0,
/**
* The countdown format.
*
* Available tags:
* {years}
* {months}
* {days}
* {hours}
* {minutes}
* {seconds}
*/
'format' => '{days} days, {hours} hours, {minutes} minutes and {seconds} seconds',
/**
* The countdown theme.
*
* Available themes:
* default
* oneline
* custom
*/
'theme' => 'default',
/**
* Set the action once countdown finishes.
*
* Available values:
* keep - Keep the countdown visible
* hide - Hide the countdown
* restart - Restart the countdown
* message - Show a message
* redirect - Redirect to a URL
*/
'countdown_action' => 'keep',
/**
* The message appearing after the countdown has finished.
*
* Requires `countdown_action` to be set to `message`
*
* Example: Countdown finished.
*/
'finish_text' => '',
/**
* The redirect URL once the countdown expires.
*
* Requires `countdown_action` to be set to `redirect`
*/
'redirect_url' => '',
/**
* Widget Settings
*/
// Gap
'gap' => 20,
// Background Color
'background_color' => '',
/**
* Unit Display Settings
*/
// Whether to display Days
'days' => true,
// Days Label
'days_label' => 'Days',
// Whether to display Hours
'hours' => true,
// Hours Label
'hours_label' => 'Hrs',
// Whether to display Minutes
'minutes' => true,
// Minutes Label
'minutes_label' => 'Mins',
// Whether to display Seconds
'seconds' => true,
// Seconds Label
'seconds_label' => 'Secs',
// Whether to display a separator between the units
'separator' => false,
// Whether to display numbers in 00 or 0 format
'double_zeroes_format' => true,
/**
* Unit Item Settings
*/
// The size (width, height) of the unit item in pixels
'item_size' => null,
// Each item padding
'item_padding' => null,
// The unit item border width
'item_border_width' => '',
// The unit item border style
'item_border_style' => '',
// The unit item border color
'item_border_color' => '',
// The unit item border radius
'item_border_radius' => null,
// Item Background Color
'item_background_color' => '',
/**
* Unit Digits Container Settings
*/
// Digits wrapper Min Width
'digits_wrapper_min_width' => 0,
// The digits wrapper padding
'digits_wrapper_padding' => null,
// The digits wrapper border radius
'digits_wrapper_border_radius' => null,
// The digits wrapper background color.
'digits_wrapper_background_color' => '',
/**
* Unit Digit Settings
*/
// Digits Font Size
'digits_font_size' => 25,
// Digits Font Weight
'digits_font_weight' => '400',
// Digit Min Width
'digit_min_width' => 0,
// The digits padding
'digits_padding' => null,
// The digits border radius
'digit_border_radius' => null,
// Digits Gap
'digits_gap' => null,
// Digit Item Background Color. This applies for each of the 2 digits on a unit.
'digit_background_color' => '',
// Digit Item Text Color
'digit_text_color' => '',
/**
* Unit Label Settings
*/
// Label Font Size
'label_font_size' => 13,
// Label Font Weight
'label_font_weight' => '400',
// Unit Label Margin Top. The spacing between the unit and its label.
'unit_label_margin_top' => 5,
// Unit Label Color
'unit_label_text_color' => '',
// Extra attributes added to the widget
'atts' => '',
// TODO: Remove in the future
'css_vars' => [],
// Preview HTML used prior to JS initializing the Countdown
'preview_html' => ''
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
Text::script('NR_AND_LC');
$this->prepare();
if ($this->options['load_css_vars'] && $this->options['theme'] !== 'custom')
{
$this->options['custom_css'] .= $this->getWidgetCSS();
/**
* TODO: Remove in the future
*
* For compatibility purposes for old customers using old
* ACF version used by ACF Previewer which is required to
* style the Countdown in the previewer.
*/
$this->options['css_vars'] = $this->options['custom_css'];
}
}
/**
* Prepares the countdown.
*
* @return void
*/
private function prepare()
{
$this->options['css_class'] .= ' is-preview ' . $this->options['theme'];
if (!empty($this->options['value']) && $this->options['value'] !== '0000-00-00 00:00:00')
{
if ($this->options['countdown_type'] === 'static' && $this->options['timezone'] === 'server')
{
// Get timezone
$tz = new \DateTimeZone(Factory::getApplication()->getCfg('offset', 'UTC'));
// Convert given date time to UTC
$this->options['value'] = date_create($this->options['value'], $tz)->setTimezone(new \DateTimeZone('UTC'))->format('c');
// Apply server timezone
$this->options['value'] = (new \DateTime($this->options['value']))->setTimezone($tz)->format('c');
}
}
$this->options['preview_html'] = $this->getPreviewHTML();
// Set countdown payload
$payload = [
'data-countdown-type="' . $this->options['countdown_type'] . '"',
'data-value="' . $this->options['value'] . '"',
'data-timezone="' . $this->options['timezone'] . '"',
'data-separator="' . (json_decode($this->options['separator']) ? 'true' : 'false') . '"',
'data-double-zeroes-format="' . (json_decode($this->options['double_zeroes_format']) ? 'true' : 'false') . '"',
'data-dynamic-days="' . $this->options['dynamic_days'] . '"',
'data-dynamic-hours="' . $this->options['dynamic_hours'] . '"',
'data-dynamic-minutes="' . $this->options['dynamic_minutes'] . '"',
'data-dynamic-seconds="' . $this->options['dynamic_seconds'] . '"',
'data-finish-text="' . htmlspecialchars($this->options['finish_text']) . '"',
'data-redirect-url="' . $this->options['redirect_url'] . '"',
'data-theme="' . $this->options['theme'] . '"',
'data-countdown-action="' . $this->options['countdown_action'] . '"',
'data-days="' . (json_decode($this->options['days']) ? 'true' : 'false') . '"',
'data-days-label="' . $this->options['days_label'] . '"',
'data-hours="' . (json_decode($this->options['hours']) ? 'true' : 'false') . '"',
'data-hours-label="' . $this->options['hours_label'] . '"',
'data-minutes="' . (json_decode($this->options['minutes']) ? 'true' : 'false') . '"',
'data-minutes-label="' . $this->options['minutes_label'] . '"',
'data-seconds="' . (json_decode($this->options['seconds']) ? 'true' : 'false') . '"',
'data-seconds-label="' . $this->options['seconds_label'] . '"'
];
// Only set the format for custom-themed countdown instances
if ($this->options['theme'] === 'custom')
{
$payload[] = 'data-format="' . htmlspecialchars($this->options['format']) . '"';
}
$this->options['atts'] = implode(' ', $payload);
}
/**
* Returns the CSS for the widget.
*
* @param array $exclude_breakpoints Define breakpoints to exclude their CSS
*
* @return string
*/
public function getWidgetCSS($exclude_breakpoints = [])
{
$controls = [
// CSS Variables
[
'property' => '--digits-background-color',
'value' => $this->options['digits_wrapper_background_color']
],
[
'property' => '--background-color',
'value' => $this->options['background_color']
],
[
'property' => '--item-background-color',
'value' => $this->options['item_background_color']
],
[
'property' => '--unit-label-text-color',
'value' => $this->options['unit_label_text_color']
],
[
'property' => '--digit-background-color',
'value' => $this->options['digit_background_color']
],
[
'property' => '--digit-text-color',
'value' => $this->options['digit_text_color']
],
[
'property' => '--unit-label-margin-top',
'value' => $this->options['unit_label_margin_top'],
'unit' => 'px'
],
[
'property' => '--digits-wrapper-min-width',
'value' => $this->options['digits_wrapper_min_width'],
'unit' => 'px'
],
[
'property' => '--digit-min-width',
'value' => $this->options['digit_min_width'],
'unit' => 'px'
],
[
'property' => '--digits-font-weight',
'value' => $this->options['digits_font_weight']
],
[
'property' => '--label-font-weight',
'value' => $this->options['label_font_weight']
],
[
'property' => '--item-border',
'type' => 'Border',
'value' => [
'width' => $this->options['item_border_width'],
'style' => $this->options['item_border_style'],
'color' => $this->options['item_border_color'],
'unit' => 'px'
]
],
// CSS
[
'type' => 'Spacing',
'property' => '--item-padding',
'value' => $this->options['item_padding'],
'unit' => 'px'
],
[
'type' => 'Spacing',
'property' => '--digits-padding',
'value' => $this->options['digits_wrapper_padding'],
'unit' => 'px'
],
[
'property' => '--gap',
'value' => $this->options['gap'],
'unit' => 'px'
],
[
'property' => '--digits-gap',
'value' => $this->options['digits_gap'],
'unit' => 'px'
],
[
'property' => '--item-size',
'value' => $this->options['item_size'],
'unit' => 'px'
],
[
'property' => '--digits-font-size',
'value' => $this->options['digits_font_size'],
'unit' => 'px'
],
[
'property' => '--label-font-size',
'value' => $this->options['label_font_size'],
'unit' => 'px'
],
[
'type' => 'Spacing',
'property' => '--digit-padding',
'value' => $this->options['digits_padding'],
'unit' => 'px'
],
[
'type' => 'Spacing',
'property' => '--item-border-radius',
'value' => $this->options['item_border_radius'],
'unit' => 'px'
],
[
'type' => 'Spacing',
'property' => '--digits-border-radius',
'value' => $this->options['digits_wrapper_border_radius'],
'unit' => 'px'
],
[
'type' => 'Spacing',
'property' => '--digit-border-radius',
'value' => $this->options['digit_border_radius'],
'unit' => 'px'
],
];
$selector = '.nrf-countdown.' . $this->options['id'];
$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
if (!$controlsCSS = $controlsInstance->generateCSS($controls))
{
return;
}
return $controlsCSS;
}
/**
* Returns preview HTML.
*
* @return string
*/
private function getPreviewHTML()
{
if ($this->options['theme'] === 'custom')
{
return $this->options['format'];
}
$format_items = [
'days' => $this->options['days'],
'hours' => $this->options['hours'],
'minutes' => $this->options['minutes'],
'seconds' => $this->options['seconds']
];
$html = '';
foreach ($format_items as $key => $value)
{
$labelStr = !empty($this->options[$key . '_label']) ? '<span class="countdown-digit-label">' . $this->options[$key . '_label'] . '</span>' : '';
$html .= '<span class="countdown-item"><span class="countdown-digit ' . $key . '"><span class="digit-number digit-1">0</span><span class="digit-number digit-2">0</span></span>' . $labelStr . '</span>';
}
return $html;
}
/**
* Returns all CSS files.
*
* @param string $theme
*
* @return array
*/
public static function getCSS($theme = 'default')
{
$css = [];
if ($theme !== 'custom')
{
$css[] = 'plg_system_nrframework/widgets/countdown.css';
}
else
{
$css[] = 'plg_system_nrframework/widgets/widget.css';
}
return $css;
}
/**
* Returns all JS files.
*
* @param string $theme
*
* @return array
*/
public static function getJS()
{
return [
'plg_system_nrframework/widgets/countdown.js'
];
}
} Widgets/FacebookVideo.php 0000644 00000004173 15235314576 0011403 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class FacebookVideo extends Video
{
/**
* Widget default options
*
* @var array
*/
protected $video_widget_options = [
// Whether we allow fullscreen
'fs' => false,
// Set to include the text from the Facebook post associated with the video, if any. Only available for desktop sites.
'show_text' => false,
// Set to show captions (if available) by default. Captions are only available on desktop.
'show_captions' => false,
];
/**
* We use the video widget layout file.
*
* @return string
*/
public function getName()
{
return 'video';
}
protected function prepare()
{
$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
// Abort
if ($videoProvider !== 'facebookvideo')
{
$this->options['value'] = null;
return;
}
$this->options['css_class'] .= ' facebookvideo';
$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';
$atts = [
'data-video-id="' . $videoID . '"',
'data-video-type="' . $videoProvider . '"',
'data-video-width="auto"',
'data-video-show-text="' . var_export($this->options['show_text'], true) . '"',
'data-video-show-captions="' . var_export($this->options['show_captions'], true) . '"',
'data-video-fs="' . var_export($this->options['fs'], true) . '"',
'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"'
];
$this->options['atts'] = implode(' ', $atts);
}
/**
* Loads media files
*
* @return void
*/
public function videoAssets()
{
HTMLHelper::script('plg_system_nrframework/widgets/video/facebookvideo.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/OpenStreetMap.php 0000644 00000002446 15235314576 0011432 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class OpenStreetMap extends Map
{
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
parent::loadMedia();
HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.min.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/vendor/leaflet.min.js', ['relative' => true, 'version' => 'auto']);
if ($this->options['load_stylesheet'])
{
HTMLHelper::stylesheet('plg_system_nrframework/widgets/openstreetmap.css', ['relative' => true, 'version' => 'auto']);
}
if ($this->options['view'] === 'satellite')
{
HTMLHelper::script('plg_system_nrframework/vendor/esri-leaflet.min.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/vendor/esri-leaflet-vector.min.js', ['relative' => true, 'version' => 'auto']);
}
HTMLHelper::script('plg_system_nrframework/widgets/openstreetmap.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/Dailymotion.php 0000644 00000005335 15235314576 0011174 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2020 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
class Dailymotion extends Video
{
/**
* Widget default options
*
* @var array
*/
protected $video_widget_options = [
// Start the video from X seconds
'start' => null,
// End the video at X seconds
'end' => null,
// Loop
'loop' => false,
// Mute
'mute' => false,
// Whether controls will appear in the video
'controls' => false,
/**
* Set the cover image type.
*
* Allowed Values:
* - none
* - auto
* - custom
*/
'coverImageType' => 'none',
// The Cover Image URL when coverImage="custom"
'coverImage' => '',
];
/**
* Prepares the widget.
*
* @return void
*/
protected function prepare()
{
$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
// Abort
if ($videoProvider !== 'dailymotion')
{
$this->options['value'] = null;
return;
}
$this->options['css_class'] .= ' dailymotion';
$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';
if ($this->options['coverImageType'] === 'auto')
{
$this->options['coverImage'] = 'url("https://www.dailymotion.com/thumbnail/video/' . $videoID . '")';
}
else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
{
$coverImage = explode('#', $this->options['coverImage']);
$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
}
$atts = [
'data-video-id="' . $videoID . '"',
'data-video-type="' . $videoProvider . '"',
'data-video-mute="' . var_export($this->options['mute'], true) . '"',
'data-video-loop="' . var_export($this->options['loop'], true) . '"',
'data-video-start="' . $this->options['start'] . '"',
'data-video-end="' . $this->options['end'] . '"',
'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
];
$this->options['atts'] = implode(' ', $atts);
}
/**
* We use the video widget layout file.
*
* @return string
*/
public function getName()
{
return 'video';
}
/**
* Loads media files
*
* @return void
*/
public function videoAssets()
{
HTMLHelper::script('plg_system_nrframework/widgets/video/dailymotion.js', ['relative' => true, 'version' => 'auto']);
}
} Widgets/BingMap.php 0000644 00000001334 15235314576 0010214 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
class BingMap extends Map
{
/**
* Loads media files
*
* @return void
*/
public function loadMedia()
{
parent::loadMedia();
HTMLHelper::script('plg_system_nrframework/widgets/bingmap.js', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('https://www.bing.com/api/maps/mapcontrol?callback=TFBingMapsCallback&key=' . $this->options['provider_key']);
}
} Conditions/Migrator.php 0000644 00000014633 15235314576 0011174 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions;
use NRFramework\Assignments;
defined('_JEXEC') or die;
class Migrator
{
/**
* Migrate old Assignments data to the new Condition Builder object.
*
* @since 5.0.1
*
* @param object $box
*
* @return void
*/
public static function run(&$params)
{
if ($params->get('mirror') == '1')
{
$params->set('display_conditions_type', 'mirror');
return;
}
$assignmentsClass = new Assignments();
$matching_method_map = [
'and' => 'all',
'or' => 'any'
];
$rules = [
0 => [
'matching_method' => $matching_method_map[$params->get('assignmentMatchingMethod', 'and')],
'enabled' => 1,
'rules' => []
]
];
foreach ($params as $paramKey => $paramValue)
{
if (strpos($paramKey, 'assign_') !== 0)
{
continue;
}
$oldName = str_replace('assign_', '', $paramKey);
$newName = $assignmentsClass->aliasToClassname($oldName);
// Skip unknown conditions
if (!$newName)
{
continue;
}
// Skip disabled conditions
if ($paramValue == '0')
{
continue;
}
// Date assignment doesn't use the value property
if ($newName == 'Date\Date')
{
$params->set($paramKey . '_list', true);
$publish_up = $params->get('assign_'. $oldName .'_param_publish_up');
$publish_down = $params->get('assign_'. $oldName .'_param_publish_down');
\NRFramework\Functions::fixDateOffset($publish_up);
\NRFramework\Functions::fixDateOffset($publish_down);
$params->set('assign_'. $oldName .'_param_publish_up', $publish_up);
$params->set('assign_'. $oldName .'_param_publish_down', $publish_down);
}
// Date assignment doesn't use the value property
if ($newName == 'Date\Time')
{
$params->set($paramKey . '_list', true);
}
// Skip conditions with no value
if (!$value = $params->get($paramKey . '_list'))
{
continue;
}
$operator = $paramValue == '1' ? 'includes' : 'not_includes';
// These Conditions have custom operators
if (in_array($newName, ['Date\Date', 'Date\Time']))
{
$operator = $paramValue == '1' ? 'equal' : 'not_equal';
}
if ($newName == 'Cookie')
{
$operatorMap = [
'exists' => 'exists',
'not_exists' => 'not_exists',
'equal' => 'equal',
'not_equal' => 'not_equal',
'contains' => 'includes',
'not_contains' => 'not_includes',
'starts' => 'starts_with',
'not_start' => 'not_starts_with',
'ends' => 'ends_with',
'not_end' => 'not_ends_with',
];
if ($paramValue == '2')
{
switch ($value)
{
case 'exists':
$value = 'not_exists';
break;
case 'equal':
$value = 'not_equal';
break;
case 'contains':
$value = 'not_contains';
break;
case 'starts':
$value = 'not_start';
break;
case 'ends':
$value = 'not_end';
break;
}
}
$operator = $operatorMap[$value];
$params->set('assign_cookiename_param_operator', $operator);
$value = $params->get('assign_cookiename_param_name');
}
if ($newName == 'Pageviews')
{
$operatorMap = [
'exactly' => 'equal',
'not_equal' => 'not_equal',
'fewer' => 'less_than',
'greater' => 'greater_than',
];
if ($paramValue == '2')
{
switch ($value)
{
case 'exactly':
$value = 'not_equal';
break;
case 'fewer':
$value = 'greater';
break;
case 'greater':
$value = 'fewer';
break;
}
}
$operator = $operatorMap[$value];
$value = $params->get('assign_pageviews_param_views');
}
$data = [
'name' => $newName,
'enabled' => 1,
'operator' => $operator,
'value' => $value
];
// Find params
foreach ($params as $assignParamKey => $assignParamValue)
{
if (strpos($assignParamKey, $paramKey . '_param') !== 0)
{
continue;
}
if ($assignParamValue == '')
{
continue;
}
$realParamName = str_replace($paramKey . '_param_', '', $assignParamKey);
$data['params'][$realParamName] = $assignParamValue;
}
$rules[0]['rules'][] = $data;
}
if (!empty($rules[0]['rules']))
{
// Finally, set the rules
$params->set('display_conditions_type', 'custom');
$params->set('rules', $rules);
}
}
} Conditions/ConditionsHelper.php 0000644 00000016070 15235314576 0012656 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions;
use NRFramework\Factory;
defined('_JEXEC') or die;
/**
* Conditions Helper Class
*
* Singleton
*/
class ConditionsHelper
{
/**
* Factory object
*
* @var \NRFramework\Factory
*/
protected $factory;
/**
* Class constructor
*/
public function __construct($factory = null)
{
$this->factory = is_null($factory) ? new Factory() : $factory;
}
/**
* Get only one instance of the class
*
* @return object
*/
static public function getInstance($factory = null)
{
static $instance = null;
if ($instance === null)
{
$instance = new ConditionsHelper($factory);
}
return $instance;
}
/**
* Passes a set of groups which are connected with OR comparison operator.
*
* Expected object:
*
* $groups = [
* [
* mathing_method => string (all|any),
* rules => array
* ],
* [
* mathing_method => string (all|any),
* rules => array
* ]
* ...
* ];
*
* @param array $groups
*
* @return mixed On validation error return null, if validation runs return bool
*/
public function passSets($groups)
{
$pass = null;
// Validations
if (!is_array($groups) OR (is_array($groups) AND empty($groups)))
{
return $pass;
}
foreach ($groups as $group)
{
// Skip invalid groups
if (!isset($group['rules']) OR !is_array($group['rules']) OR (is_array($group['rules']) AND empty($group['rules'])))
{
continue;
}
$matching_method = isset($group['matching_method']) ? $group['matching_method'] : 'all';
// If a group meets the condition, pass the check and abort so no further tests are executed.
if ($pass = $this->passSet($group['rules'], $matching_method))
{
break;
}
}
return $pass;
}
/**
* Passes a set of rules.
*
* Expected object for rules:
*
* $rules = [
* [
* name => string,
* value => mixed,
* operator => string,
* params => array
* ],
* [
* name => string,
* value => mixed,
* operator => string,
* params => array
* ]
* ...
* ];
*
* @param array $rules
* @param string $matchingMethod
*
* @return bool
*/
public function passSet($rules, $matchingMethod)
{
$pass = null;
// Validations
if (!is_array($rules) OR (is_array($rules) AND empty($rules)))
{
return $pass;
}
foreach ($rules as $rule)
{
// Skip unknown rules
if (!isset($rule['name']))
{
continue;
}
// Validate rule
$params = isset($rule['params']) ? $rule['params'] : null;
$value = isset($rule['value']) ? $rule['value'] : '';
$operator = isset($rule['operator']) ? $rule['operator'] : '';
// Run checks
$pass = $this->passOne($rule['name'], $value, $operator, $params);
// Check no further the Ruleset when any of the following happens:
// 1. We expect ALL Rules to pass but one fails.
// 2. We expect ANY Rule to pass and one does so.
if ((!$pass AND $matchingMethod == 'all') OR ($pass AND $matchingMethod == 'any'))
{
break;
}
}
return $pass;
}
/**
* Execute given rnule
*
* @param string $name The name of the rule. Case-sensitive.
* @param mixed $selection The value to compare with the value returned by the rule.
* @param string $operator The operator to use to do the comparison
* @param array $params Optional rule parameters
* @return mixed Null when the validation doesn't run properly, bool otherwize
*/
public function passOne($name, $selection, $operator, $params = [])
{
// Convert deprecated operators to new operators
$migrationMap = [
'is' => 'not_empty',
'is_not' => 'empty',
];
if (array_key_exists($operator, $migrationMap))
{
$operator = $migrationMap[$operator];
}
if (!$rule = $this->getCondition($name, $selection, str_replace('not_', '', $operator ?? ''), $params))
{
return;
}
$pass = $rule->pass();
if (is_null($pass))
{
return $pass;
}
return strpos($operator ?? '', 'not_') !== false ? !$pass : $pass;
}
/**
* Initialize the condition class object
*
* @param string $name The name of the rule. Case-sensitive.
* @param mixed $selection The value to compare with the value returned by the rule.
* @param string $operator The operator to use to do the comparison
* @param array $params Optional rule parameters
* @return mixed Null on failure, object on success
*/
public function getCondition($name, $selection = null, $operator = '', $params = null)
{
if (!$name)
{
return;
}
$class = __NAMESPACE__ . '\\Conditions\\' . $name;
if (!class_exists($class))
{
return;
}
// Prepare rule options
$options = [
'selection' => $selection,
'operator' => str_replace('not_', '', $operator),
'params' => $params
];
$rule = new $class($options, $this->factory);
return $rule;
}
/**
* Validate and manipulate rules before they are get stored into the database.
*
* @param array $rules
*
* @return void
*/
public function onBeforeSave(&$rules)
{
// If its a string, transform it into an array, otherwise, use the actual value (array)
$rules = is_string($rules) ? json_decode($rules, true) : $rules;
if (!is_array($rules))
{
return;
}
foreach ($rules as &$group)
{
if (!isset($group['rules']))
{
continue;
}
foreach ($group['rules'] as &$rule)
{
if (!$condition = $this->getCondition($rule['name']))
{
continue;
}
if (!\method_exists($condition, 'onBeforeSave'))
{
continue;
}
$condition->onBeforeSave($rule);
}
}
}
} Conditions/ConditionBuilder.php 0000644 00000027704 15235314576 0012650 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions;
defined('_JEXEC') or die;
use Joomla\CMS\Layout\LayoutHelper;
use NRFramework\Conditions\ConditionsHelper;
use NRFramework\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Form\Form;
class ConditionBuilder
{
public static function pass($rules)
{
$rules = self::prepareRules($rules);
if (empty($rules))
{
return true;
}
return ConditionsHelper::getInstance()->passSets($rules);
}
/**
* Prepare rules object to run checks
*
* @return void
*/
public static function prepareRules($rules = [])
{
if (!is_array($rules))
{
return [];
}
$rules_ = [];
foreach ($rules as $key => $group)
{
if (isset($group['enabled']) AND !(bool) $group['enabled'])
{
continue;
}
// A group without rules, doesn't make sense.
if (!isset($group['rules']) OR (isset($group['rules']) AND empty($group['rules'])))
{
continue;
}
$validRules = [];
foreach ($group['rules'] as $rule)
{
// Make sure rule has a name.
if (!isset($rule['name']) OR (isset($rule['name']) AND empty($rule['name'])))
{
continue;
}
// Rule is invalid if both value and params properties are empty
if (!isset($rule['value']) && !isset($rule['params']))
{
continue;
}
// Skip disabled rules
if (isset($rule['enabled']) && !(bool) $rule['enabled'])
{
continue;
}
// We don't need this property.
unset($rule['enabled']);
// Prepare rule value if necessary
if (isset($rule['value']))
{
$rule['value'] = self::prepareTFRepeaterValue($rule['value']);
}
// Verify operator
if (!isset($rule['operator']) OR (isset($rule['operator']) && empty($rule['operator'])))
{
$rule['operator'] = isset($rule['params']['operator']) ? $rule['params']['operator'] : '';
}
$validRules[] = $rule;
}
if (count($validRules) > 0)
{
$group['rules'] = $validRules;
if (!isset($group['matching_method']) OR (isset($group['matching_method']) AND empty($group['matching_method'])))
{
$group['matching_method'] = 'all';
}
unset($group['enabled']);
$rules_[] = $group;
}
}
return $rules_;
}
/**
* Parse the value of the TF Repeater Input field.
*
* @param array $selection
*
* @return mixed
*/
public static function prepareTFRepeaterValue($selection)
{
// Only proceed when we have an array of arrays selection.
if (!is_array($selection))
{
return $selection;
}
$first = array_values($selection)[0];
if (!is_array($first))
{
return $selection;
}
if (!isset($first['value']))
{
return $selection;
}
$new_selection = [];
foreach ($selection as $value)
{
/**
* We expect a `value` key for TFInputRepeater fields or a key,value pair
* for plain arrays.
*/
if (!isset($value['value']))
{
/**
* If no value exists, it means that the passed $assignment->selection is a key,value pair array so we use the value
* as our returned selection.
*
* This happens when we pass a key,value pair array as $assignment->selection when we expect a TFInputRepeater value
* so we need to take this into consideration.
*/
$new_selection[] = $value;
continue;
}
// value must not be empty
if (is_scalar($value['value']) && empty(trim($value['value'])))
{
continue;
}
$new_selection[] = count($value) === 1 ? $value['value'] : $value;
}
return $new_selection;
}
/**
* Returns the TGeoIP plugin modal.
*
* @return string
*/
public static function getGeoModal()
{
// Do not proceed if the database is up-to-date
if (!\NRFramework\Extension::geoPluginNeedsUpdate())
{
return;
}
HTMLHelper::_('bootstrap.modal');
$modalName = 'tf-geodbchecker-modal';
// The TGeoIP Plugin URL
$url = Uri::base(true) . '/index.php?option=com_plugins&view=plugin&tmpl=component&layout=modal&extension_id=' . \NRFramework\Functions::getExtensionID('tgeoip', 'system');
$options = [
'title' => Text::_('NR_EDIT'),
'url' => $url,
'height' => '400px',
'backdrop' => 'static',
'bodyHeight' => '70',
'modalWidth' => '70',
'footer' => '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-dismiss="modal" aria-hidden="true">'
. Text::_('JLIB_HTML_BEHAVIOR_CLOSE') . '</button>
<button type="button" class="btn btn-success" aria-hidden="true"
onclick="jQuery(\'#' . $modalName . ' iframe\').contents().find(\'#applyBtn\').click();">'
. Text::_('JAPPLY') . '</button>',
];
return HTMLHelper::_('bootstrap.renderModal', $modalName, $options);
}
/**
* Prepares the given rules list.
*
* @param array $list
*
* @return array
*/
public static function prepareXmlRulesList($list)
{
if (is_array($list))
{
$list = implode(',', array_map('trim', $list));
}
else if (is_string($list))
{
$list = str_replace(' ', '', $list);
}
return $list;
}
/**
* Adds a new condition item or group.
*
* @param string $controlGroup The name of the input used to store the data.
* @param string $groupKey The group index ID.
* @param string $conditionKey The added condition item index ID.
* @param array $condition The condition name we are adding.
* @param string $include_rules The list of included conditions that override the available conditions.
* @param string $exclude_rules The list of excluded conditions that override the available conditions.
* @param bool $exclude_rules_pro Whether the excluded rules should appear as Pro missing features.
*
* @return string
*/
public static function add($controlGroup, $groupKey, $conditionKey, $condition = null, $include_rules = [], $exclude_rules = [], $exclude_rules_pro = false)
{
$controlGroup_ = $controlGroup . "[$groupKey][rules][$conditionKey]"; // @Todo - rename input namespace to 'conditions'
$form = self::getForm('conditionbuilder/base.xml', $controlGroup_, $condition);
$form->setFieldAttribute('name', 'include_rules', is_array($include_rules) ? implode(',', $include_rules) : $include_rules);
$form->setFieldAttribute('name', 'exclude_rules', is_array($exclude_rules) ? implode(',', $exclude_rules) : $exclude_rules);
$form->setFieldAttribute('name', 'exclude_rules_pro', $exclude_rules_pro);
$options = [
'name' => $controlGroup_,
'enabled' => !isset($condition['enabled']) ? true : (string) $condition['enabled'] == '1',
'toolbar' => $form,
'groupKey' => $groupKey,
'conditionKey' => $conditionKey,
'options' => ''
];
if (isset($condition['name']))
{
$optionsHTML = self::renderOptions($condition['name'], $controlGroup_, $condition);
$options['condition_name'] = $condition['name'];
$options['options'] = $optionsHTML;
}
return self::getLayout('conditionbuilder_row', $options);
}
/**
* Render condition item settings.
*
* @param string $name The name of the condition item.
* @param string $controlGroup The name of the input used to store the data.
* @param object $formData The data that will be bound to the form.
*
* @return string
*/
public static function renderOptions($name, $controlGroup = null, $formData = null)
{
if (!$form = self::getForm('conditions/' . strtolower(str_replace('\\', '/', $name)) . '.xml', $controlGroup, $formData))
{
return;
}
$form->setFieldAttribute('note', 'ruleName', $name);
return $form->renderFieldset('general');
}
/**
* Handles loading condition builder given a payload.
*
* @param array $payload
*
* @return string
*/
public static function initLoad($payload = [])
{
if (!$payload)
{
return;
}
if (!isset($payload['data']) &&
!isset($payload['name']))
{
return;
}
if (!$data = json_decode($payload['data']))
{
return;
}
// transform object to assosiative array
$data = json_decode(json_encode($data), true);
// html of condition builder
$html = '';
$include_rules = isset($payload['include_rules']) ? $payload['include_rules'] : [];
$exclude_rules = isset($payload['exclude_rules']) ? $payload['exclude_rules'] : [];
$exclude_rules_pro = isset($payload['exclude_rules_pro']) ? $payload['exclude_rules_pro'] : false;
foreach ($data as $groupKey => $groupConditions)
{
$payload = [
'name' => $payload['name'],
'groupKey' => $groupKey,
'groupConditions' => $groupConditions,
'include_rules' => $include_rules,
'exclude_rules' => $exclude_rules,
'exclude_rules_pro' => $exclude_rules_pro
];
$html .= self::getLayout('conditionbuilder_group', $payload);
}
return $html;
}
/**
* Render a layout given its name and payload.
*
* @param string $name
* @param array $payload
*
* @return string
*/
public static function getLayout($name, $payload)
{
return LayoutHelper::render($name, $payload, JPATH_PLUGINS . '/system/nrframework/layouts');
}
/**
* Returns the form by binding given data.
*
* @param string $name
* @param string $controlGroup
* @param array $data
*
* @return object
*/
private static function getForm($name, $controlGroup, $data = null)
{
if (!file_exists(JPATH_PLUGINS . '/system/nrframework/xml/' . $name))
{
return;
}
$form = new Form('cb', ['control' => $controlGroup]);
$form->addFieldPath(JPATH_PLUGINS . '/system/nrframework/fields');
$form->loadFile(JPATH_PLUGINS . '/system/nrframework/xml/' . $name);
if (!is_null($data))
{
$form->bind($data);
}
return $form;
}
} Conditions/Condition.php 0000644 00000022715 15235314576 0011336 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions;
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Language\Text;
/**
* Assignment Class
*/
class Condition
{
/**
* Application Object
*
* @var object
*/
protected $app;
/**
* Document Object
*
* @var object
*/
protected $doc;
/**
* Date Object
*
* @var object
*/
protected $date;
/**
* Database Object
*
* @var object
*/
protected $db;
/**
* User Object
*
* @var object
*/
protected $user;
/**
* Assignment Selection
*
* @var mixed
*/
protected $selection;
/**
* Assignment Parameters
*
* @var mixed
*/
protected $params;
/**
* Assignment State (Include|Exclude)
*
* @var string
*/
public $assignment;
/**
* Options
*
* @var object
*/
public $options;
/**
* Framework factory object
*
* @var object
*/
public $factory;
/**
* The default operator that will be used to compare haystack with needle.
*
* @var string
*/
protected $operator;
/**
* Class constructor
*
* @param array $options The rule options. Expected properties: selection, value, params
* @param object $factory The framework's factory class.
*/
public function __construct($options = null, $factory = null)
{
$this->factory = is_null($factory) ? new \NRFramework\Factory() : $factory;
// Set General Joomla Objects
$this->db = $this->factory->getDbo();
$this->app = $this->factory->getApplication();
$this->doc = $this->factory->getDocument();
$this->user = $this->factory->getUser();
$this->options = new Registry($options);
$this->setParams($this->options->get('params'));
$this->setOperator($this->options->get('operator', 'includesSome'));
// For performance reasons we might move this inside the pass() method
$this->setSelection($this->options->get('selection', ''));
}
/**
* Set the rule's user selected value
*
* @param mixed $selection
* @return object
*/
public function setSelection($selection)
{
$this->selection = $selection;
if (method_exists($this, 'prepareSelection'))
{
$this->selection = $this->prepareSelection();
}
return $this;
}
/**
* Undocumented function
*
* @return void
*/
public function getSelection()
{
return $this->selection;
}
/**
* Set the operator that will be used for the comparison
*
* @param string $operator
* @return object
*/
public function setOperator($operator)
{
$this->operator = $operator;
return $this;
}
/**
* Set the rule's parameters
*
* @param array $params
*/
public function setParams($params)
{
$this->params = new Registry($params);
}
public function getParams()
{
return $this->params;
}
/**
* Checks the validitity of two values based on the given operator.
*
* Consider converting this method as a Trait.
*
* @param mixed $value
* @param mixed $selection
* @param string $operator
* @param array $options ignoreCase: true,false
*
* @return bool
*/
public function passByOperator($value = null, $selection = null, $operator = null, $options = null)
{
$value = is_null($value) ? $this->value() : $value;
if (!is_null($selection))
{
$this->setSelection($selection);
}
$selection = $this->getSelection();
$options = new Registry($options);
$ignoreCase = $options->get('ignoreCase', true);
if (is_object($value))
{
$value = (array) $value;
}
if (is_object($selection))
{
$selection = (array) $selection;
}
if ($ignoreCase)
{
if (is_string($value))
{
$value = strtolower($value);
}
if (is_string($selection))
{
$selection = strtolower($selection);
}
if (is_array($value))
{
$value = array_map('strtolower', $value);
}
if (is_array($selection))
{
$selection = array_map(function($str)
{
return is_null($str) ? '' : strtolower($str);
}, $selection);
}
}
$operator = (is_null($operator) OR empty($operator)) ? $this->operator : $operator;
$pass = false;
switch ($operator)
{
case 'exists':
$pass = !is_null($value);
break;
// Determines whether haystack is empty. Accepts: array, string
case 'empty':
if (is_array($value))
{
$pass = empty($value);
}
if (is_string($value))
{
$pass = $value == '' || trim($value) == '';
}
if (is_bool($value))
{
$pass = !$value;
}
break;
case 'equals':
if (is_array($selection) || is_array($value))
{
$pass = $this->passByOperator($value, $selection, 'includesSome', $options);
}
else
{
$pass = $value == $selection;
}
break;
case 'contains':
if (is_string($value) && is_string($selection))
{
$pass = strlen($selection) > 0 && strpos($value, $selection) !== false;
}
break;
// Determine whether haystack is less than needle.
case 'less_than':
case 'lowerthan':
case 'lt':
$pass = $value < $selection;
break;
// Determine whether haystack is less than or equal to needle.
case 'less_than_or_equal_to':
case 'lowerthanequal':
case 'lte':
$pass = $value <= $selection;
break;
// Determine whether haystack is greater than needle.
case 'greater_than':
case 'greaterthan':
case 'gt':
$pass = $value > $selection;
break;
// Determine whether haystack is greater than or equal to needle.
case 'greater_than_or_equal_to':
case 'greterthanequal':
case 'gte':
$pass = $value >= $selection;
break;
// Determine whether haystack contains all elements in needle.
case 'includesAll':
case 'containsall':
$pass = count(array_intersect((array) $selection, (array) $value)) == count((array) $selection);
break;
// Determine whether haystack contains at least one element from needle.
case 'includesSome':
case 'containsany':
$pass = !empty(array_intersect((array) $value, (array) $selection));
break;
// Determine whether haystack contains at least one element from needle. Accepts; string, array.
case 'includes':
if (is_string($value) && $value != '' && is_string($selection) && $selection != '')
{
if (StringHelper::strpos($value, $selection) !== false)
{
$pass = true;
}
}
if (is_array($value) || is_array($selection))
{
$pass = $this->passByOperator($value, $selection, 'includesSome', $options);
}
break;
// Determine whether haystack starts with needle. Accepts: string
case 'starts_with':
$pass = StringHelper::substr($value, 0, StringHelper::strlen($selection)) === $selection;
break;
// Determine whether haystack ends with needle. Accepts: string
case 'ends_with':
$pass = StringHelper::substr($value, -StringHelper::strlen($selection)) === $selection;
break;
// Determine whether value is in given range
case 'range':
$value1 = isset($selection['value1']) ? (float) $selection['value1'] : false;
$value2 = isset($selection['value2']) ? (float) $selection['value2'] : false;
$pass = $value1 && $value2 ? (($value >= $value1) && ($value <= $value2)) : false;
break;
// Determine whether haystack equals to needle. Accepts any object.
default:
$pass = $value == $selection;
}
return $pass;
}
/**
* Base assignment check
*
* @return bool
*/
public function pass()
{
return $this->passByOperator();
}
/**
* Returns all parent rows
*
* This method doesn't belong here. Move it to Functions.php.
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'menu', $parent = 'parent_id', $child = 'id')
{
if (!$id)
{
return [];
}
$cache = $this->factory->getCache();
$hash = md5('getParentIds_' . $id . '_' . $table . '_' . $parent . '_' . $child);
if ($cache->has($hash))
{
return $cache->get($hash);
}
$parent_ids = array();
while ($id)
{
$query = $this->db->getQuery(true)
->select('t.' . $parent)
->from('#__' . $table . ' as t')
->where('t.' . $child . ' = ' . (int) $id);
$this->db->setQuery($query);
$id = $this->db->loadResult();
// Break if no parent is found or parent already found before for some reason
if (!$id || in_array($id, $parent_ids))
{
break;
}
$parent_ids[] = $id;
}
return $cache->set($hash, $parent_ids);
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
$value = $this->value();
// If the rule returns an array, use the 1st one.
$value = is_array($value) ? $value[0] : $value;
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_' . strtoupper($this->getName()), ucfirst(strtolower($value)));
}
/**
* Return the rule name
*
* @return string
*/
protected function getName()
{
$classParts = explode('\\', get_called_class());
return array_pop($classParts);
}
} Conditions/Conditions/URL.php 0000644 00000001006 15235314576 0012151 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
class URL extends URLBase
{
/**
* Returns the assignment's value
*
* @return string Current URL
*/
public function value()
{
return $this->factory->getURL();
}
} Conditions/Conditions/EngageBox.php 0000644 00000002771 15235314576 0013360 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class EngageBox extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['onotherbox'];
/**
* Checks if the user viewed any of the given boxes
*
* @return bool
*/
public function pass()
{
// Skip if the visitorID is not set
$visitorID = \NRFramework\VisitorToken::getInstance()->get();
if (empty($visitorID))
{
return true;
}
$box_ids = $this->selection;
if (!is_array($box_ids) || empty($box_ids))
{
return true;
}
$box_ids = implode(',', $box_ids);
$query = $this->db->getQuery(true);
$query
->select('COUNT(id)')
->from($this->db->quoteName('#__rstbox_logs'))
->where($this->db->quoteName('event') . ' = 1')
->where($this->db->quoteName('box') . " IN ( $box_ids )")
->where($this->db->quoteName('visitorid') . ' = '. $this->db->quote($visitorID));
$this->db->setQuery($query);
$pass = (int) $this->db->loadResult();
return (bool) $pass;
}
} Conditions/Conditions/Cookie.php 0000644 00000002564 15235314576 0012732 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Cookie extends Condition
{
/**
* When we need to compare the user's value with the cookie value, we change the $selection to the value entered by the user.
*
* @return void
*/
public function prepareSelection()
{
if (in_array($this->operator, ['exists', 'empty']))
{
return $this->getSelection();
}
return $this->params->get('content', '');
}
/**
* Return the value of the cookie as stored in the user's browser
*
* @return string The value of the cookie
*/
public function value()
{
/**
* $this->selection is not used here as prepareSelection() above, called in \NRFramework\Conditions\Condition->setSelection() method changes its value
* and thus we do not always have the correct Cookie Name to search for.
*
* $this->options->get('selection') will always have the correct cookie name.
*/
return $this->factory->getCookie($this->options->get('selection'));
}
} Conditions/Conditions/TimeOnSite.php 0000644 00000002550 15235314576 0013534 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class TimeOnSite extends Condition
{
/**
* Returns the assignment's value
*
* @return int Time on site in seconds
*/
public function value()
{
return $this->getTimeOnSite();
}
/**
* Returns the user's time on site in seconds
*
* @return int
*/
public function getTimeOnSite()
{
if (!$sessionStartTime = strtotime($this->getSessionStartTime()))
{
return;
}
$dateTimeNow = strtotime(\NRFramework\Functions::dateTimeNow());
return $dateTimeNow - $sessionStartTime;
}
/**
* Returns the sessions start time
*
* @return string
*/
private function getSessionStartTime()
{
$session = $this->factory->getSession();
$var = 'starttime';
$sessionStartTime = $session->get($var);
if (!$sessionStartTime)
{
$date = \NRFramework\Functions::dateTimeNow();
$session->set($var, $date);
}
return $session->get($var);
}
} Conditions/Conditions/ReturningNewVisitor.php 0000644 00000001370 15235314576 0015522 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
/**
* @deprecated Use the NewVisitor condition instead.
*/
class ReturningNewVisitor extends Condition
{
public function pass()
{
// Get visitor instance
$visitor = new \NRFramework\Visitor();
// Create and update cookies as needed
$visitor->createOrUpdateCookie();
// Check if user is new
$isNew = $visitor->isNew();
return $this->operator === 'new' ? $isNew : !$isNew;
}
} Conditions/Conditions/AcyMailing.php 0000644 00000005060 15235314576 0013530 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class AcyMailing extends Condition
{
/**
* Returns the assignment's value
*
* @return array AcyMailing lists
*/
public function value()
{
return $this->getSubscribedLists();
}
/**
* Returns all AcyMailing lists the user is subscribed to
*
* @return array AcyMailing lists
*/
private function getSubscribedLists()
{
if (!$user = $this->user->id)
{
return false;
}
// Get a db connection.
$db = $this->db;
// Create a new query object.
$query = $db->getQuery(true);
$lists = [];
// Read AcyMailing v5 lists
if (\NRFramework\Extension::isInstalled('com_acymailing'))
{
$query
->select(array('list.listid'))
->from($db->quoteName('#__acymailing_listsub', 'list'))
->join('INNER', $db->quoteName('#__acymailing_subscriber', 'sub') . ' ON (' . $db->quoteName('list.subid') . '=' . $db->quoteName('sub.subid') . ')')
->where($db->quoteName('list.status') . ' = 1')
->where($db->quoteName('sub.userid') . ' = ' . $user)
->where($db->quoteName('sub.confirmed') . ' = 1')
->where($db->quoteName('sub.enabled') . ' = 1');
// Reset the query using our newly populated query object.
$db->setQuery($query);
if ($cols = $db->loadColumn())
{
$lists = array_merge($lists, $cols);
}
}
// Read AcyMailing > v5 lists
if (\NRFramework\Extension::isInstalled('com_acym'))
{
// Create a new query object.
$query = $db->getQuery(true);
$query
->select(['list.id'])
->from($db->quoteName('#__acym_user_has_list', 'userlist'))
->join('INNER', $db->quoteName('#__acym_list', 'list') . ' ON (' . $db->quoteName('list.id') . '=' . $db->quoteName('userlist.list_id') . ')')
->join('INNER', $db->quoteName('#__acym_user', 'user') . ' ON (' . $db->quoteName('user.id') . '=' . $db->quoteName('userlist.user_id') . ')')
->where($db->quoteName('user.cms_id') . ' = ' . $user)
->where($db->quoteName('userlist.status') . ' = 1')
->where($db->quoteName('userlist.unsubscribe_date') . ' IS NULL');
// Reset the query using our newly populated query object.
$db->setQuery($query);
$cols = $db->loadColumn();
foreach ($cols as $value)
{
$lists[] = '6:' . $value;
}
}
return $lists;
}
}
Conditions/Conditions/OS.php 0000644 00000002212 15235314576 0012030 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\WebClient;
use NRFramework\Functions;
use NRFramework\Conditions\Condition;
class OS extends Condition
{
/**
* Check the client's operating system
*
* @return bool
*/
public function prepareSelection()
{
$selection = Functions::makeArray($this->getSelection());
// backwards compatibility check
// replace 'iphone' and 'ipad' selection values with 'ios'
return array_map(function($os_selection)
{
if ($os_selection === 'iphone' || $os_selection === 'ipad')
{
return 'ios';
}
return $os_selection;
}, $selection);
}
/**
* Returns the assignment's value
*
* @return string OS name
*/
public function value()
{
return WebClient::getOS();
}
} Conditions/Conditions/Pageviews.php 0000644 00000001341 15235314576 0013443 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Pageviews extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['user.pageviews'];
/**
* Returns the assignment's value
*
* @return int Number of page visits
*/
public function value()
{
return $this->factory->getSession()->get('session.counter', 0);
}
} Conditions/Conditions/ConvertFormsForm.php 0000644 00000002015 15235314576 0014763 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class ConvertFormsForm extends Condition
{
/**
* Returns the condition value.
*
* @return array
*/
public function value()
{
return $this->getForms();
}
/**
* Returns all form IDs submitted by the visitor.
* If the user is logged in, we try to get the forms by user's ID
* Otherwise, the visitor cookie ID will be used instead.
*
* @return array
*/
private function getForms()
{
$class = '\ConvertForms\Helper';
if (!class_exists($class))
{
return;
}
// Sanity check
if (!method_exists($class, 'getVisitorSubmittedForms'))
{
return;
}
return $class::getVisitorSubmittedForms();
}
} Conditions/Conditions/URLBase.php 0000644 00000004074 15235314576 0012754 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Functions;
use NRFramework\Conditions\Condition;
class URLBase extends Condition
{
public function prepareSelection()
{
return Functions::makeArray($this->getSelection());
}
/**
* Pass URL.
*
* @return bool Returns true if the current URL contains any of the selection URLs
*/
public function pass()
{
return $this->passURL();
}
/**
* Pass URL
*
* @param mixed $url If null, the current URL will be used. Otherwise we need a valid absolute URL.
*
* @return bool Returns true if the URL contains any of the selection URLs
*/
public function passURL($url = null)
{
// Get the current URL if none is passed
$url = is_null($url) ? $this->factory->getURL() : $url;
// Create an array with all possible values of the URL
$urls = array(
html_entity_decode(urldecode($url), ENT_COMPAT, 'UTF-8'),
urldecode($url),
html_entity_decode($url, ENT_COMPAT, 'UTF-8'),
$url
);
// Remove duplicates and invalid URLs
$urls = array_filter(array_unique($urls));
$regex = $this->params->get('regex', false);
$pass = false;
foreach ($urls as $url)
{
foreach ($this->getSelection() as $s)
{
// Skip empty selection URLs
$s = trim($s);
if (empty($s))
{
continue;
}
// Regular expression check
if ($regex)
{
$url_part = str_replace(array('#', '&'), array('\#', '(&|&)'), $s);
$s = '#' . $url_part . '#si';
if (@preg_match($s . 'u', $url) || @preg_match($s, $url))
{
$pass = true;
break;
}
continue;
}
// String check
if (strpos($url, $s) !== false)
{
$pass = true;
break;
}
}
if ($pass)
{
break;
}
}
return $pass;
}
} Conditions/Conditions/ConvertForms.php 0000644 00000001742 15235314576 0014145 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class ConvertForms extends Condition
{
/**
* Returns the assignment's value
*
* @return array List of campaign IDs
*/
public function value()
{
return $this->getCampaigns();
}
/**
* Returns campaigns list visitor is subscribed to
* If the user is logged in, we try to get the campaigns by user's ID
* Otherwise, the visitor cookie ID will be used instead
*
* @return array List of campaign IDs
*/
private function getCampaigns()
{
$class = '\ConvertForms\Helper';
if (!class_exists($class))
{
return;
}
return $class::getVisitorCampaigns();
}
} Conditions/Conditions/Referrer.php 0000644 00000001411 15235314576 0013263 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
class Referrer extends URLBase
{
/**
* Pass Referrer URL.
*
* @return bool Returns true if the Referrer URL contains any of the selection URLs
*/
public function pass()
{
return $this->passURL($this->value());
}
/**
* Returns the assignment's value
*
* @return string Referrer URL
*/
public function value()
{
return $this->app->input->server->get('HTTP_REFERER', '', 'RAW');
}
} Conditions/Conditions/Browser.php 0000644 00000001062 15235314576 0013134 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Browser extends Condition
{
/**
* Returns the assignment's value
*
* @return string Browser name
*/
public function value()
{
return $this->factory->getBrowser()['name'];
}
} Conditions/Conditions/NewVisitor.php 0000644 00000001304 15235314576 0013621 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class NewVisitor extends Condition
{
public static $shortcode_aliases = ['isnewvisitor'];
/**
* Checks whether the visitor is new or returning
*
* @return boolean True when visitor is new
*/
public function value()
{
$visitor = new \NRFramework\Visitor();
$visitor->createOrUpdateCookie();
return $visitor->isNew();
}
} Conditions/Conditions/PHP.php 0000644 00000001255 15235314576 0012144 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class PHP extends Condition
{
/**
* Pass check Custom PHP
*
* @return bool
*/
public function pass()
{
return (bool) $this->value();
}
public function value()
{
// Enable buffer output
ob_start();
$pass = $this->factory->getExecuter($this->selection)->run();
ob_end_clean();
return $pass;
}
} Conditions/Conditions/AkeebaSubs.php 0000644 00000002622 15235314576 0013521 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class AkeebaSubs extends Condition
{
/**
* Returns the assignment's value
*
* @return array Akeeba Subscriptions
*/
public function value()
{
return $this->getlevels();
}
/**
* Returns all user's active subscriptions
*
* @param int $userid User's id
*
* @return array Akeeba Subscriptions
*/
private function getLevels()
{
if (!$user = $this->user->id)
{
return false;
}
if (!defined('FOF30_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof30/include.php'))
{
return false;
}
// Get the Akeeba Subscriptions container. Also includes the autoloader.
$container = \FOF30\Container\Container::getInstance('com_akeebasubs');
$subscriptionsModel = $container->factory->model('Subscriptions')->tmpInstance();
$items = $subscriptionsModel
->user_id($user)
->enabled(1)
->get();
if (!$items->count())
{
return false;
}
$levels = array();
foreach ($items as $subscription)
{
$levels[] = $subscription->akeebasubs_level_id;
}
return array_unique($levels);
}
} Conditions/Conditions/Device.php 0000644 00000001523 15235314576 0012712 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use Joomla\CMS\Language\Text;
class Device extends Condition
{
/**
* Returns the assignment's value
*
* @return string Device type
*/
public function value()
{
return $this->factory->getDevice();
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
return parent::getValueHint() . ' ' . Text::_('NR_ASSIGN_DEVICES_NOTE');
}
} Conditions/Conditions/Homepage.php 0000644 00000001240 15235314576 0013234 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use Joomla\CMS\Factory;
class Homepage extends Condition
{
public static $shortcode_aliases = ['ishomepage'];
public function value()
{
$menu = Factory::getApplication()->getMenu();
$lang = Factory::getLanguage()->getTag();
return ($menu->getActive() == $menu->getDefault($lang));
}
} Conditions/Conditions/IP.php 0000644 00000006314 15235314576 0012026 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions;
defined('_JEXEC') or die;
use NRFramework\User;
use NRFramework\Functions;
use NRFramework\Conditions\Condition;
class IP extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['ip_address', 'iprange'];
public function prepareSelection()
{
return Functions::makeArray($this->getSelection());
}
/**
* Checks if the user's ip address is within the specified ranges
*
* @return bool
*/
public function pass()
{
// get the user's ip address
$user_ip = $this->value();
// get the supplied ip addresses/ranges as an array
foreach ($this->getSelection() as $ip_range)
{
if ($this->isInRange($user_ip, $ip_range))
{
return true;
}
}
return false;
}
/**
* Returns the assignment's value
*
* @return string User IP
*/
public function value()
{
return User::getIP();
}
/**
* Checks if an IP address falls within an IP range
* Todo: factor out common logic...
* @param string $user_ip
* @param string $range
* @return boolean
*/
protected function isInRange($user_ip, $range)
{
if (empty($user_ip) || empty($range))
{
return false;
}
// break ip addresses/ranges into parts
$user_ip_parts = explode('.', $user_ip);
$ip_range_parts = explode('.', $range);
for ($i = 0; $i < count($ip_range_parts); $i++)
{
$r = $ip_range_parts[$i];
// parse and check range
if (strpos($r, '-') !== FALSE)
{
list($range_start, $range_end) = explode('-', $r);
// format checks...
if (!is_numeric($range_start) || !is_numeric($range_end))
{
return false;
}
// cast strings to integers
$range_start = (int) $range_start;
$range_end = (int) $range_end;
if ($range_start > $range_end || $range_start < 0 || $range_end > 255)
{
return false;
}
if ((int)$user_ip_parts[$i] < $range_start || (int)$user_ip_parts[$i] > $range_end)
{
return false;
}
}
else
{
// format checks...
if (!is_numeric($r))
{
return false;
}
$r = (int)$r;
if ($r < 0 || $r > 255)
{
return false;
}
if ((int)$user_ip_parts[$i] !== $r)
{
return false;
}
}
} //for loop
return true;
}
} Conditions/Conditions/Geo/Continent.php 0000644 00000002310 15235314576 0014161 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Geo;
defined('_JEXEC') or die;
use NRFramework\Functions;
class Continent extends GeoBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['geo.continent'];
/**
* Continent check
*
* @return bool
*/
public function prepareSelection()
{
$selection = Functions::makeArray($this->getSelection());
// Try to convert continent names to codes
return array_map(function($c) {
if (strlen($c) > 2)
{
$c = \NRFramework\Continents::getCode($c);
}
return $c;
}, $selection);
}
/**
* Return the Continent's code and full name
*
* @return string Country code
*/
public function value()
{
return [
$this->geo->getContinentName('en'),
$this->geo->getContinentCode()
];
}
} Conditions/Conditions/Geo/City.php 0000644 00000001200 15235314576 0013125 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Geo;
defined('_JEXEC') or die;
class City extends GeoBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['geo.city'];
/**
* Returns the assignment's value
*
* @return string City name
*/
public function value()
{
return $this->geo->getCity();
}
} Conditions/Conditions/Geo/GeoBase.php 0000644 00000004744 15235314576 0013542 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Geo;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use NRFramework\User;
use Joomla\CMS\Language\Text;
/**
* IP addresses sample
*
* Greece / Dodecanese: 94.67.238.3
* Belgium / Flanders: 37.62.255.255
* USA / New York: 72.229.28.185
*/
class GeoBase extends Condition
{
/**
* GeoIP Class
*
* @var class
*/
protected $geo;
/**
* Indicates whether we detected successfully the user's geographical location
*
* @var bool
*/
protected $success;
/**
* Class constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options = null, $factory = null)
{
parent::__construct($options, $factory);
$ip = $this->params->get('ip', null);
$this->loadGeo($ip);
}
/**
* Load GeoIP Classes
*
* @return void
*/
private function loadGeo($ip)
{
if (!class_exists('TGeoIP'))
{
$path = JPATH_PLUGINS . '/system/tgeoip';
if (@file_exists($path . '/helper/tgeoip.php'))
{
if (@include_once($path . '/vendor/autoload.php'))
{
@include_once $path . '/helper/tgeoip.php';
}
}
// If for some reason the tgeoip plugin files do not exist, abort
if (!class_exists('TGeoIP'))
{
return;
}
}
$this->geo = new \TGeoIP($ip);
$record = $this->geo->getRecord();
$this->success = ($record !== false AND !is_null($record));
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
if (!$this->success)
{
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_GEO_ERROR', User::getIP());
}
// If the rule returns an array, use the 1st one.
$value = $this->value();
$value = is_array($value) ? $value[0] : $value;
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_GEO', User::getIP(), $this->getName(), ucfirst(strtolower($value)));
}
} Conditions/Conditions/Geo/Region.php 0000644 00000002650 15235314576 0013452 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Geo;
defined('_JEXEC') or die;
class Region extends GeoBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['geo.region'];
/**
* Returns the assignment's value
*
* @return string Region codes
*/
public function value()
{
return $this->getRegions();
}
/**
* Get list of all ISO 3611 Country Region Codes
*
* @return array
*/
private function getRegions()
{
$regionCodes = [];
$record = $this->geo->getRecord();
if ($record === false || is_null($record))
{
return $regionCodes;
}
// Skip if no regions found
if (!$regions = $record->subdivisions)
{
return $regionCodes;
}
foreach ($regions as $key => $region)
{
// Get the Region's full name
$regionCodes[] = $region->names['en'];
// Get the Region's code by preppending the country isocode to the region code
$regionCodes[] = $record->country->isoCode . '-' . $region->isoCode;
}
return $regionCodes;
}
} Conditions/Conditions/Geo/Country.php 0000644 00000002211 15235314576 0013663 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Geo;
defined('_JEXEC') or die;
use NRFramework\Countries;
use NRFramework\Functions;
class Country extends GeoBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['geo.country'];
/**
* Country check
*
* @return bool
*/
public function prepareSelection()
{
$selection = Functions::makeArray($this->getSelection());
return array_map(function($c) {
if (strlen($c) > 2)
{
$c = Countries::getCode($c);
}
return $c;
}, $selection);
}
/**
* Returns the assignment's value
*
* @return string Country code
*/
public function value()
{
return [
$this->geo->getCountryName(),
$this->geo->getCountryCode()
];
}
} Conditions/Conditions/Component/HikashopCartContainsProducts.php 0000644 00000001425 15235314576 0021261 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCartContainsProducts extends HikashopBase
{
public function prepareSelection()
{
return $this->getPreparedSelection();
}
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['hikashop.cart_contains_products'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passProductsInCart(['product_id', 'cart_product_parent_id'], 'cart_product_quantity');
}
} Conditions/Conditions/Component/JShoppingSingle.php 0000644 00000001402 15235314576 0016514 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JShoppingSingle extends JShoppingBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jshopping.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/VirtueMartCategoryView.php 0000644 00000001260 15235314576 0020106 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCategoryView extends VirtueMartBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
if (!$this->isCategoryPage())
{
return false;
}
$this->params->set('view_category', true);
$this->params->set('view_single', false);
return $this->passCategories('virtuemart_categories', 'category_parent_id');
}
} Conditions/Conditions/Component/JCalProCategory.php 0000644 00000001205 15235314576 0016442 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JCalProCategory extends JCalProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jcalpro.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('categories', 'parent_id');
}
} Conditions/Conditions/Component/HikashopCategoryView.php 0000644 00000002211 15235314576 0017547 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCategoryView extends HikashopBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
if (!$this->isCategoryPage())
{
return false;
}
$this->params->set('view_category', true);
$this->params->set('view_single', false);
return $this->passCategories('hikashop_category', 'category_parent_id');
}
/**
* Returns all parent rows
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'hikashop_category', $parent = 'category_parent_id', $child = 'category_id')
{
return parent::getParentIds($id, $table, $parent, $child);
}
} Conditions/Conditions/Component/JShoppingBase.php 0000644 00000002725 15235314576 0016156 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JShoppingBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'product';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_jshopping';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->view = $this->app->input->get('view', $this->app->input->get('controller'));
$this->request->id = $this->app->input->getInt('product_id');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__jshopping_products_to_categories')
->where($db->quoteName('product_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/ICagendaSingle.php 0000644 00000001377 15235314576 0016261 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ICagendaSingle extends ICagendaBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['icagenda.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/VirtueMartCurrentProductPrice.php 0000644 00000000767 15235314576 0021457 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCurrentProductPrice extends VirtueMartBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCurrentProductPrice();
}
} Conditions/Conditions/Component/ContentView.php 0000644 00000002315 15235314576 0015722 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ContentView extends ContentBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['contentview'];
/**
* Pass check for Joomla! Articles
*
* @return bool
* @return bool
*/
public function pass()
{
// Make sure we are in the right context
if (empty($this->selection) || !$this->passContext())
{
return false;
}
// In the Joomla Content component, the 'view' query parameter equals to 'category' in both Category List and Category Blog views.
// In order to distinguish them we are using the 'layout' parameter as well.
if ($this->request->view == 'category' && $this->request->layout)
{
$this->request->view .= '_' . $this->request->layout;
}
return $this->passByOperator($this->request->view, $this->selection, 'includes');
}
} Conditions/Conditions/Component/DPCalendarCategory.php 0000644 00000001021 15235314576 0017101 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DPCalendarCategory extends DPCalendarBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('categories', 'parent_id');
}
} Conditions/Conditions/Component/RSEventsProSingle.php 0000644 00000001410 15235314576 0017004 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSEventsProSingle extends RSEventsProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['rseventspro.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/ICagendaCategory.php 0000644 00000002026 15235314576 0016605 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ICagendaCategory extends ICagendaBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['icagenda.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('icagenda_category', '');
}
/*
* Returns all parent rows
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'icagenda_category', $parent = '', $child = '')
{
return [];
}
} Conditions/Conditions/Component/VirtueMartCategory.php 0000644 00000001242 15235314576 0017253 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCategory extends VirtueMartBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['virtuemart.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('virtuemart_categories', 'category_parent_id');
}
} Conditions/Conditions/Component/JCalProBase.php 0000644 00000002073 15235314576 0015543 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JCalProBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_jcalpro';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__jcalpro_event_categories')
->where($db->quoteName('event_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/SPPageBuilderSingle.php 0000644 00000001422 15235314576 0017243 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SPPageBuilderSingle extends SPPageBuilderBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['sppagebuilder.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/ZooBase.php 0000644 00000003343 15235314576 0015021 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ZooBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'item';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_zoo';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->view = $this->app->input->get('view', $this->app->input->get('task'));
// Normally the item's id can be read by the request parameters BUT if the item
// is assosiated to a menu item the item_id parameter is not yet available and
// we can only find it out through the menu's parameters.
$this->request->id = (int) $this->app->input->getInt('item_id', $this->app->getMenu()->getActive()->getParams()->get('item_id'));
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__zoo_category_item')
->where($db->quoteName('item_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/ZooCategory.php 0000644 00000001167 15235314576 0015726 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ZooCategory extends ZooBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['zoo.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('zoo_category', 'parent');
}
} Conditions/Conditions/Component/EasyBlogBase.php 0000644 00000002054 15235314576 0015755 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EasyBlogBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'entry';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_easyblog';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__easyblog_post')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/ZooSingle.php 0000644 00000001360 15235314576 0015365 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ZooSingle extends ZooBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['zoo.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/DJEventsSingle.php 0000644 00000001377 15235314576 0016310 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJEventsSingle extends DJEventsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djevents.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/HikashopBase.php 0000644 00000010470 15235314576 0016017 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikaShopBase extends EcommerceBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'product';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_hikashop';
/**
* The request ID used to retrieve the ID of the product
*
* @var string
*/
protected $request_id = 'product_id';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->id = $this->app->input->get('cid', $this->app->input->getInt('product_id'));
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__hikashop_product_category')
->where($db->quoteName('product_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
/**
* Returns Hikashop cart data
*
* @return mixed
*/
protected function getCart()
{
@include_once(implode(DIRECTORY_SEPARATOR, [JPATH_ADMINISTRATOR, 'components', 'com_hikashop', 'helpers', 'helper.php']));
@include_once(implode(DIRECTORY_SEPARATOR, [JPATH_ADMINISTRATOR, 'components', 'com_hikashop', 'helpers', 'checkout.php']));
if (!class_exists('hikashopCheckoutHelper'))
{
return;
}
$checkoutHelper = \hikashopCheckoutHelper::get();
return $checkoutHelper->getCart(true);
}
/**
* Returns the products in the cart
*
* @return array
*/
protected function getCartProducts()
{
if (!$cart = $this->getCart())
{
return [];
}
return $cart->cart_products;
}
/**
* Returns the current user's last purchase date in format: d/m/Y H:i:s and in UTC.
*
* @param int $user_id
*
* @return string
*/
protected function getLastPurchaseDate($user_id = null)
{
if (!$user_id)
{
return;
}
$db = $this->db;
$query = $this->db->getQuery(true)
->clear()
->select('o.order_created')
->from('#__hikashop_order_product AS op')
->leftJoin('#__hikashop_order AS o ON o.order_id = op.order_id')
->leftJoin('#__hikashop_user AS u ON u.user_id = o.order_user_id')
->where('o.order_status IN ("confirmed", "shipped")')
->where('u.user_cms_id = ' . (int) $user_id)
->order('o.order_created DESC')
->setLimit(1);
$db->setQuery($query);
return $db->loadResult();
}
/**
* Returns the current product.
*
* @return object
*/
protected function getCurrentProduct()
{
if (!$this->request->id)
{
return;
}
if (!function_exists('hikashop_get'))
{
return;
}
$productClass = hikashop_get('class.product');
return $productClass->get($this->request->id);
}
/**
* Returns the current product data.
*
* @return object
*/
protected function getCurrentProductData()
{
if (!$product = $this->getCurrentProduct())
{
return;
}
return [
'id' => $product->product_id,
'price' => (float) $product->product_msrp
];
}
/**
* Returns the product stock.
*
* @param int $id
*
* @return int
*/
public function getProductStock($id = null)
{
if (!$id)
{
return;
}
if (!function_exists('hikashop_get'))
{
return;
}
$productClass = hikashop_get('class.product');
if (!$product = $productClass->get($id))
{
return;
}
// Means infinite
if ($product->product_quantity === -1)
{
return PHP_INT_MAX;
}
return (int) $product->product_quantity;
}
} Conditions/Conditions/Component/JCalProSingle.php 0000644 00000001407 15235314576 0016112 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JCalProSingle extends JCalProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/JBusinessDirectoryBusinessSingle.php 0000644 00000001466 15235314576 0022133 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryBusinessSingle extends JBusinessDirectoryBusinessBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.business_single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/VirtueMartCartContainsProducts.php 0000644 00000001407 15235314576 0021615 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCartContainsProducts extends VirtueMartBase
{
public function prepareSelection()
{
return $this->getPreparedSelection();
}
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['virtumart.cart_contains_products'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passProductsInCart(['virtuemart_product_id', 'product_parent_id']);
}
} Conditions/Conditions/Component/JReviewsBase.php 0000644 00000003544 15235314576 0016013 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JReviewsBase extends ComponentBase
{
protected $viewSingle = 'article';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_content';
/**
* Indicates whether the page is a category page
*
* @return boolean
*/
protected function isCategory()
{
return is_null($this->request->task);
}
/**
* Indicates whether the page is a single page
*
* @return boolean
*/
public function isSinglePage()
{
if (!class_exists('\ClassRegistry'))
{
return;
}
if (!$listingModel = \ClassRegistry::getClass('EverywhereComContentModel'))
{
return;
}
if (!$listing = $listingModel->getListingById($this->request->id))
{
return;
}
return $this->request->view === 'article' && $this->request->option === 'com_content' && $listing;
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
if (!class_exists('\ClassRegistry'))
{
return;
}
if (!$listingModel = \ClassRegistry::getClass('EverywhereComContentModel'))
{
return;
}
if (!$listing = $listingModel->getListingById($id))
{
return;
}
return isset($listing['Category']['cat_id']) ? $listing['Category']['cat_id'] : null;
}
} Conditions/Conditions/Component/DJCatalog2Single.php 0000644 00000001405 15235314576 0016470 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJCatalog2Single extends DJCatalog2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djcatalog2.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/GridboxBase.php 0000644 00000002053 15235314576 0015645 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class GridboxBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'page';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_gridbox';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('page_category')
->from('#__gridbox_pages')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/VirtueMartLastPurchasedDate.php 0000644 00000000762 15235314576 0021044 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartLastPurchasedDate extends VirtueMartBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passLastPurchaseDate();
}
} Conditions/Conditions/Component/EshopCategory.php 0000644 00000001216 15235314576 0016230 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EshopCategory extends EshopBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['eshop.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('eshop_categories', 'category_parent_id');
}
} Conditions/Conditions/Component/JBusinessDirectoryBase.php 0000644 00000002160 15235314576 0020040 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'companies';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_jbusinessdirectory';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('categoryId'))
->from('#__jbusinessdirectory_company_category')
->where($db->quoteName('companyId') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/DJClassifiedsSingle.php 0000644 00000001416 15235314576 0017267 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJClassifiedsSingle extends DJClassifiedsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djclassifieds.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/RSEventsProBase.php 0000644 00000002250 15235314576 0016440 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSEventsProBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'rseventspro';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_rseventspro';
/**
* Get single events's assosiated categories
*
* @param Integer The Single Event id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('id')
->from('#__rseventspro_taxonomy')
->where($db->quoteName('ide') . '=' . $db->q($id))
->where($db->quoteName('type') . '=' . $db->q('category'));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/HikashopCategory.php 0000644 00000002165 15235314576 0016724 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCategory extends HikashopBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['hikashopcategory'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('hikashop_category', 'category_parent_id');
}
/**
* Returns all parent rows
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'hikashop_category', $parent = 'category_parent_id', $child = 'category_id')
{
return parent::getParentIds($id, $table, $parent, $child);
}
} Conditions/Conditions/Component/EcommerceBase.php 0000644 00000020267 15235314576 0016155 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class EcommerceBase extends ComponentBase
{
/**
* Pass method for "Amount In Cart" condition.
*
* @return bool
*/
public function passAmountInCart()
{
// Whether we exclude shipping cost
$exclude_shipping_cost = $this->params->get('exclude_shipping_cost', '0') === '1';
$shipping_total = 0;
$amount = 0;
switch ($this->params->get('total', 'total'))
{
case 'total':
$amount = $this->getCartTotal();
if ($exclude_shipping_cost)
{
$shipping_total = -$this->getShippingTotal();
}
break;
case 'subtotal':
$amount = $this->getCartSubtotal();
if (!$exclude_shipping_cost)
{
$shipping_total = $this->getShippingTotal();
}
break;
}
// Calculate final amount
$amount = $amount + $shipping_total;
$operator = $this->options->get('operator', 'equal');
$selection = (float) $this->selection;
// Range selection
if ($operator === 'range')
{
$selection = [
'value1' => $selection,
'value2' => (float) $this->options->get('params.value2', false)
];
}
return $this->passByOperator($amount, $selection, $operator);
}
/**
* Pass method for "Products In Cart" condition.
*
* @param string $cart_product_item_id_key
*
* @return bool
*/
protected function passProductsInCart($cart_product_item_id_key = ['id'], $product_prop_key = 'quantity')
{
// Get cart products
if (!$cartProducts = $this->getCartProducts())
{
return false;
}
// Get condition products
if (!$conditionProducts = $this->selection)
{
return false;
}
if (!is_array($conditionProducts))
{
return false;
}
// Ensure all condition's products exist in the cart
$foundCartProducts = array_filter(
$cartProducts,
function ($prod) use ($conditionProducts, $cart_product_item_id_key, $product_prop_key)
{
$prod = (array) $prod;
// Check the ID first
foreach ($cart_product_item_id_key as $id_key)
{
$valid = array_filter($conditionProducts, function($item) use ($prod, $id_key) {
return isset($item['value']) && (int) $item['value'] === (int) $prod[$id_key];
});
if ($valid)
{
break;
}
}
// If not valid, abort
if (!$valid)
{
return;
}
// Get valid product
$valid_product = reset($valid);
// Ensure product has property
$product_property_value = isset($prod[$product_prop_key]) ? (int) $prod[$product_prop_key] : false;
if (!$product_property_value)
{
return $valid;
}
// We need an operator other than "any"
if (!isset($valid_product['operator']) || $valid_product['operator'] === 'any')
{
return $valid;
}
// Ensure value 1 is valid
$product_value1 = isset($valid_product['value1']) ? (int) $valid_product['value1'] : false;
if (!$product_value1)
{
return $valid;
}
$product_value2 = isset($valid_product['value2']) ? (int) $valid_product['value2'] : false;
// Default selection
$selection = $product_value1;
// Range selection
if ($valid_product['operator'] === 'range')
{
$selection = [
'value1' => $product_value1,
'value2' => $product_value2
];
}
return $this->passByOperator($product_property_value, $selection, $valid_product['operator']);
}
);
return count($foundCartProducts);
}
/**
* Pass method for "Last Purchase Date" condition.
*
* @return bool
*/
protected function passLastPurchaseDate()
{
if (!$user = Factory::getUser())
{
return;
}
if (!$user->id)
{
return;
}
if (!$purchase_date = $this->getLastPurchaseDate($user->id))
{
return;
}
$purchaseDate = new \DateTime('@' . $purchase_date);
$purchaseDate->setTimezone(new \DateTimeZone('UTC'));
$purchaseDate->setTime(0,0);
$currentDate = new \DateTime('now', new \DateTimeZone('UTC'));
$pass = false;
$operator = $this->options->get('params.operator', 'within_hours');
switch ($operator)
{
case 'within_hours':
case 'within_days':
case 'within_weeks':
case 'within_months':
if (!$within_value = intval($this->options->get('params.within_value')))
{
return;
}
$period = str_replace('within_', '', $operator);
$timeframe = strtoupper($period[0]);
// Hours requires a "T"
if ($timeframe === 'H')
{
$within_value = 'T' . $within_value;
}
$interval = new \DateInterval("P{$within_value}{$timeframe}");
$purchaseDateXDaysAgo = (clone $purchaseDate)->add($interval);
$interval->invert = 1; // Set invert to 1 to indicate past time
$pass = $purchaseDateXDaysAgo >= $currentDate;
break;
case 'equal':
if (!$this->selection)
{
return;
}
$selectionDate = new \DateTime($this->selection, new \DateTimeZone('UTC'));
$pass = $purchaseDate->format('Y-m-d') === $selectionDate->format('Y-m-d');
break;
case 'before':
if (!$this->selection)
{
return;
}
$selectionDate = new \DateTime($this->selection, new \DateTimeZone('UTC'));
$pass = $purchaseDate < $selectionDate;
break;
case 'after':
if (!$this->selection)
{
return;
}
$selectionDate = new \DateTime($this->selection, new \DateTimeZone('UTC'));
$pass = $purchaseDate > $selectionDate;
break;
case 'range':
if (!$secondDate = $this->options->get('params.value2'))
{
return;
}
if (!$this->selection)
{
return;
}
$startDate = new \DateTime($this->selection, new \DateTimeZone('UTC'));
$endDate = new \DateTime($secondDate, new \DateTimeZone('UTC'));
$pass = $purchaseDate >= $startDate && $purchaseDate <= $endDate;
break;
}
return $pass;
}
/**
* Pass method for "Current Product Price" condition.
*
* @return bool
*/
public function passCurrentProductPrice()
{
// Ensure we are viewing a product page
if (!$this->isSinglePage())
{
return;
}
if (!$this->selection)
{
return;
}
// Get current product data
if (!$product_data = $this->getCurrentProductData())
{
return;
}
// Get value 1
$selection = (float) $this->options->get('selection');
// Range selection
if ($this->operator === 'range')
{
$value2 = (float) $this->options->get('params.value2');
$selection = [
'value1' => $selection,
'value2' => $value2
];
}
return $this->passByOperator($product_data['price'], $selection, $this->operator);
}
/**
* Pass method for "Current Product Stock" condition.
*
* @return bool
*/
public function passCurrentProductStock()
{
// Ensure we are viewing a product page
if (!$this->isSinglePage())
{
return;
}
if (!$this->selection)
{
return;
}
$current_product_id = $this->request->id;
if (!$product_stock = $this->getProductStock($current_product_id))
{
return;
}
// Get value 1
$selection = (int) $this->options->get('selection');
// Range selection
if ($this->operator === 'range')
{
$value2 = (int) $this->options->get('params.value2');
$selection = [
'value1' => $selection,
'value2' => $value2
];
}
return $this->passByOperator($product_stock, $selection, $this->operator);
}
protected function getPreparedSelection()
{
$selection = $this->getSelection();
if (!is_array($selection))
{
return $selection;
}
foreach ($selection as &$value)
{
if (!is_array($value))
{
continue;
}
$params = isset($value['params']) ? $value['params'] : [];
if ($params)
{
if (isset($params['value']))
{
$params['value1'] = $params['value'];
}
unset($params['value']);
unset($value['params']);
}
$value = array_merge($value, $params);
}
return $selection;
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id) {}
} Conditions/Conditions/Component/K2Category.php 0000644 00000001174 15235314576 0015431 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class K2Category extends K2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['k2_cats', 'k2category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('k2_categories', 'parent');
}
} Conditions/Conditions/Component/ContentCategory.php 0000644 00000001143 15235314576 0016563 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ContentCategory extends ContentBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories();
}
} Conditions/Conditions/Component/ICagendaBase.php 0000644 00000002070 15235314576 0015701 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ICagendaBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_icagenda';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('catid'))
->from('#__icagenda_events')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/SobiProSingle.php 0000644 00000001374 15235314576 0016200 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SobiProSingle extends SobiProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['sobipro.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/DJCatalog2Category.php 0000644 00000001205 15235314576 0017022 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJCatalog2Category extends DJCatalog2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djcatalog2.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('djc2_categories');
}
} Conditions/Conditions/Component/K2Base.php 0000644 00000006312 15235314576 0014525 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class K2Base extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'item';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_k2';
/**
* Get single page's assosiated categories
*
* @param integer The Single Page id
*
* @return integer
*/
protected function getSinglePageCategories($id)
{
$item = $this->getK2Item();
return isset($item->catid) ? $item->catid : null;
}
/**
* Indicates whether the current view concerns a Category view
*
* @return boolean
*/
protected function isCategoryPage()
{
return ($this->request->layout == 'category' || $this->request->task == 'category' || $this->request->view == 'latest');
}
/**
* Returns a K2 item
*
* @return object|null
*/
public function getK2Item()
{
$cache = $this->factory->getcache();
$hash = md5('k2assitem');
if ($cache->has($hash))
{
return $cache->get($hash);
}
// K2 doesn't have a Joomla 4+ version, bail early
if (!class_exists('JModelLegacy'))
{
return;
}
// Ignore JModelLegacy here because K2 doesn't have a J4+ version, so we don't care.
return $cache->set($hash, \JModelLegacy::getInstance('Item', 'K2Model')->getData());
}
/**
* Return tags of a K2 item
*
* @param int $id K2 item ID
*
* @return array
*/
public function getK2tags($id = null)
{
$id = is_null($id) ? $this->request->id : $id;
if (!$id)
{
return [];
}
$cache = $this->factory->getcache();
$hash = md5('k2_item_tags' . $id);
if ($cache->has($hash))
{
return $cache->get($hash);
}
$q = $this->db->getQuery(true)
->select('t.id')
->from('#__k2_tags_xref AS tx')
->join('LEFT', '#__k2_tags AS t ON t.id = tx.tagID')
->where('tx.itemID = ' . $this->db->q($id))
->where('t.published = 1');
$this->db->setQuery($q);
return $cache->set($hash, $this->db->loadColumn());
}
/**
* Get current view layout string
*
* @return string
*/
public function getPageType()
{
$view = $this->request->view;
$layout = $this->request->layout;
if (is_null($layout))
{
switch ($view)
{
case 'item':
$layout = 'item';
break;
default:
$layout = $this->request->task;
break;
}
}
$pagetype = $view . '_' . $layout;
return $pagetype;
}
} Conditions/Conditions/Component/J2StoreCategory.php 0000644 00000001152 15235314576 0016441 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class J2StoreCategory extends J2StoreBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['j2storecategory'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories();
}
} Conditions/Conditions/Component/J2StoreSingle.php 0000644 00000001373 15235314576 0016112 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class J2StoreSingle extends J2StoreBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['j2storesingle'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/SPPageBuilderCategory.php 0000644 00000001227 15235314576 0017602 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SPPageBuilderCategory extends SPPageBuilderBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['sppagebuilder.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('categories', 'parent_id');
}
} Conditions/Conditions/Component/DJCatalog2Base.php 0000644 00000002324 15235314576 0016122 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJCatalog2Base extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'item';
/**
* The component's Category Page view name
*
* @var string
*/
protected $viewCategory = 'items';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_djcatalog2';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('category_id'))
->from($db->quoteName('#__djc2_items_categories'))
->where($db->quoteName('item_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/DJClassifiedsBase.php 0000644 00000002061 15235314576 0016715 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJClassifiedsBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'item';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_djclassifieds';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('cat_id')
->from('#__djcf_items')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/VirtueMartTotalSpend.php 0000644 00000002303 15235314576 0017552 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartTotalSpend extends VirtueMartBase
{
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->options->get('selection');
}
/**
* Returns the condtion value.
*
* @return float
*/
public function value()
{
if (!$user = $this->factory->getUser())
{
return;
}
if (!$user->id)
{
return;
}
$db = $this->db;
$query = $db->getQuery(true)
->clear()
->select('SUM(paid) AS total')
->from('#__virtuemart_orders')
->where('order_status IN ("C", "S", "F")')
->where('virtuemart_user_id = ' . (int) $user->id);
$db->setQuery($query);
return round((float) $db->loadResult(), 2);
}
} Conditions/Conditions/Component/EasyBlogCategory.php 0000644 00000001217 15235314576 0016660 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EasyBlogCategory extends EasyBlogBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['easyblog.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('easyblog_category', 'parent_id');
}
} Conditions/Conditions/Component/EasyBlogSingle.php 0000644 00000001377 15235314576 0016333 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EasyBlogSingle extends EasyBlogBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['easyblog.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/HikashopCartValue.php 0000644 00000004144 15235314576 0017034 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCartValue extends HikashopBase
{
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->options->get('selection');
}
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['hikashopcartvalue'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passAmountInCart();
}
/**
* Returns the cart total.
*
* @return float
*/
public function getCartTotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (!isset($cart->full_total->prices[0]->price_value_with_tax))
{
return 0;
}
return $cart->full_total->prices[0]->price_value_with_tax;
}
/**
* Returns the cart subtotal.
*
* @return float
*/
public function getCartSubtotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (isset($cart->full_total->prices[0]->price_value_without_shipping))
{
return $cart->full_total->prices[0]->price_value_without_shipping;
}
if (isset($cart->full_total->prices[0]->price_value_without_payment))
{
return $cart->full_total->prices[0]->price_value_without_payment;
}
return 0;
}
/**
* Returns the shipping total.
*
* @return float
*/
protected function getShippingTotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (!isset($cart->shipping))
{
return 0;
}
if (!is_array($cart->shipping))
{
return 0;
}
if (!count($cart->shipping))
{
return 0;
}
$total_fees = 0;
foreach ($cart->shipping as $item)
{
$total_fees += (float) $item->shipping_price;
}
return $total_fees;
}
} Conditions/Conditions/Component/HikashopCurrentProductPrice.php 0000644 00000000763 15235314576 0021117 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCurrentProductPrice extends HikashopBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCurrentProductPrice();
}
} Conditions/Conditions/Component/JBusinessDirectoryOfferCategory.php 0000644 00000001310 15235314576 0021721 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryOfferCategory extends JBusinessDirectoryOfferBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.offer_category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('jbusinessdirectory_categories', 'parent_id');
}
} Conditions/Conditions/Component/JBusinessDirectoryEventSingle.php 0000644 00000001451 15235314576 0021413 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryEventSingle extends JBusinessDirectoryEventBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.event_single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/EventBookingCategory.php 0000644 00000001222 15235314576 0017541 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EventBookingCategory extends EventBookingBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['eventbookingcategory'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('eb_categories', 'parent');
}
} Conditions/Conditions/Component/GridboxSingle.php 0000644 00000001374 15235314576 0016221 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class GridboxSingle extends GridboxBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['gridbox.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/EshopSingle.php 0000644 00000001366 15235314576 0015702 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EshopSingle extends EshopBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['eshop.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/RSBlogCategory.php 0000644 00000001211 15235314577 0016276 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSBlogCategory extends RSBlogBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['rsblog.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('rsblog_categories', 'parent_id');
}
} Conditions/Conditions/Component/HikashopLastPurchasedDate.php 0000644 00000000756 15235314577 0020514 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopLastPurchasedDate extends HikashopBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passLastPurchaseDate();
}
} Conditions/Conditions/Component/DJClassifiedsCategory.php 0000644 00000001234 15235314577 0017622 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJClassifiedsCategory extends DJClassifiedsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djclassifieds.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('djcf_categories', 'parent_id');
}
} Conditions/Conditions/Component/DPCalendarBase.php 0000644 00000002076 15235314577 0016212 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DPCalendarBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_dpcalendar';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('catid'))
->from('#__dpcalendar_events')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/HikashopSingle.php 0000644 00000001402 15235314577 0016362 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopSingle extends HikashopBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['hikashopsingle'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/K2Tag.php 0000644 00000001562 15235314577 0014371 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class K2Tag extends K2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['k2_tags', 'k2tag'];
/**
* Pass check for K2 Tags
*
* @return bool
*/
public function pass()
{
if (empty($this->selection) || !$this->passContext())
{
return false;
}
return parent::pass();
}
/**
* Returns the assignment's value
*
* @return array K2 item tags
*/
public function value()
{
return $this->getK2tags();
}
} Conditions/Conditions/Component/HikashopTotalSpend.php 0000644 00000002414 15235314577 0017222 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopTotalSpend extends HikashopBase
{
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->options->get('selection');
}
/**
* Returns the condtion value.
*
* @return float
*/
public function value()
{
if (!$user = $this->factory->getUser())
{
return;
}
if (!$user->id)
{
return;
}
$db = $this->db;
$query = $db->getQuery(true)
->clear()
->select('SUM(o.order_full_price) AS total')
->from('#__hikashop_order AS o')
->leftJoin('#__hikashop_user AS u ON u.user_id = o.order_user_id')
->where('o.order_status IN (\'shipped\', \'confirmed\')')
->where('u.user_cms_id = ' . (int) $user->id);
$db->setQuery($query);
return (float) $db->loadResult();
}
} Conditions/Conditions/Component/DJEventsCategory.php 0000644 00000001207 15235314577 0016635 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJEventsCategory extends DJEventsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['djevents.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('djev_cats', 'parent_id');
}
} Conditions/Conditions/Component/QuixSingle.php 0000644 00000001367 15235314577 0015554 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class QuixSingle extends QuixBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['quix.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/K2Pagetype.php 0000644 00000001605 15235314577 0015432 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class K2Pagetype extends K2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['k2_pagetypes', 'k2pagetype'];
/**
* Pass check for K2 page types
*
* @return bool
*/
public function pass()
{
if (empty($this->selection) || !$this->passContext())
{
return false;
}
return parent::pass();
}
/**
* Returns the assignment's value
*
* @return string Pagetype
*/
public function value()
{
return $this->getPageType();
}
} Conditions/Conditions/Component/DPCalendarSingle.php 0000644 00000001212 15235314577 0016550 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DPCalendarSingle extends DPCalendarBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/VirtueMartSingle.php 0000644 00000001405 15235314577 0016721 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartSingle extends VirtueMartBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['virtuemart.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/JBusinessDirectoryEventCategory.php 0000644 00000001304 15235314577 0021745 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryEventCategory extends JBusinessDirectoryEventBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.event_category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('jbusinessdirectory_categories', 'parent_id');
}
} Conditions/Conditions/Component/SPPageBuilderBase.php 0000644 00000002057 15235314577 0016702 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SPPageBuilderBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'page';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_sppagebuilder';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('catid')
->from('#__sppagebuilder')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/JEventsSingle.php 0000644 00000001374 15235314577 0016202 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JEventsSingle extends JEventsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jevents.single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/VirtueMartBase.php 0000644 00000011306 15235314577 0016353 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartBase extends EcommerceBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'productdetails';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_virtuemart';
/**
* The request ID used to retrieve the ID of the product
*
* @var string
*/
protected $request_id = 'virtuemart_product_id';
/**
* The request ID used to retrieve the ID of the product category.
*
* @var string
*/
protected $category_request_id = 'virtuemart_category_id';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->id = $this->app->input->getInt($this->request_id, $this->app->input->getInt($this->category_request_id));
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('virtuemart_category_id')
->from('#__virtuemart_product_categories')
->where($db->quoteName($this->request_id) . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
/**
* Returns Virtuemart cart data
*
* @return mixed
*/
protected function getCart()
{
// load the configuration wherever required as its not available everywhere
if (!class_exists('VmConfig'))
{
@include_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/config.php';
\VmConfig::loadConfig();
}
@include_once JPATH_SITE . '/components/com_virtuemart/helpers/cart.php';
if (!class_exists('VirtueMartCart'))
{
return;
}
$cart = \VirtueMartCart::getCart();
$cart->prepareCartData();
return $cart;
}
/**
* Returns the products in the cart
*
* @return array
*/
protected function getCartProducts()
{
if (!$cart = $this->getCart())
{
return [];
}
return $cart->products;
}
/**
* Returns the current user's last purchase date in format: d/m/Y H:i:s and in UTC.
*
* @param int $user_id
*
* @return string
*/
protected function getLastPurchaseDate($user_id = null)
{
if (!$user_id)
{
return;
}
$db = $this->db;
$query = $this->db->getQuery(true)
->clear()
->select('created_on')
->from('#__virtuemart_orders')
->where('order_status IN ("C", "S", "F")')
->where('virtuemart_user_id = ' . (int) $user_id)
->order('created_on DESC')
->setLimit(1);
$db->setQuery($query);
return strtotime($db->loadResult());
}
/**
* Returns the current product.
*
* @return object
*/
protected function getCurrentProduct()
{
if (!$this->request->id)
{
return;
}
return $this->getProductById($this->request->id);
}
protected function getProductById($id = null)
{
if (!$id)
{
return;
}
if (!class_exists('VmModel'))
{
return;
}
return \VmModel::getModel('Product')->getProduct($id);
}
/**
* Returns the current product data.
*
* @return object
*/
protected function getCurrentProductData()
{
if (!$product = $this->getCurrentProduct())
{
return;
}
return [
'id' => $product->virtuemart_product_id,
'price' => isset($product->prices['salesPrice']) ? (float) $product->prices['salesPrice'] : 0
];
}
/**
* Returns the product stock.
*
* @param int $id
*
* @return int
*/
public function getProductStock($id = null)
{
if (!$id)
{
return;
}
if (!$product = $this->getProductById($id))
{
return;
}
return $product->product_in_stock;
}
/*
* Returns all parent rows
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'virtuemart_categories', $parent = 'category_parent_id', $child = 'virtuemart_category_id')
{
return parent::getParentIds($id, $table, $parent, $child);
}
} Conditions/Conditions/Component/JReviewsCategory.php 0000644 00000000763 15235314577 0016717 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JReviewsCategory extends JReviewsBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories();
}
} Conditions/Conditions/Component/SobiProCategory.php 0000644 00000001205 15235314577 0016526 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SobiProCategory extends SobiProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['sobipro.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('sobipro_relations', 'id');
}
} Conditions/Conditions/Component/JEventsCategory.php 0000644 00000001153 15235314577 0016531 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JEventsCategory extends JEventsBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jevents.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories();
}
} Conditions/Conditions/Component/JReviewsSingle.php 0000644 00000001206 15235314577 0016354 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JReviewsSingle extends JReviewsBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/ContentArticle.php 0000644 00000001426 15235314577 0016376 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class ContentArticle extends ContentBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['article'];
/**
* Pass check for Joomla! Articles
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int Article ID
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/RSBlogBase.php 0000644 00000002061 15235314577 0015377 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSBlogBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'post';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_rsblog';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('cat_id')
->from('#__rsblog_posts_categories')
->where($db->quoteName('post_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/VirtueMartCartValue.php 0000644 00000004400 15235314577 0017364 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCartValue extends VirtueMartBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['virtuemart.cart_value'];
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->options->get('selection');
}
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passAmountInCart();
}
/**
* Returns the cart total billable cost
*
* @return float
*/
protected function getCartTotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (!isset($cart->cartPrices['billTotal']))
{
return 0;
}
@include_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/currencydisplay.php';
if (!class_exists('CurrencyDisplay'))
{
return 0;
}
$currency = \CurrencyDisplay::getInstance();
return $currency->roundByPriceConfig($cart->cartPrices['billTotal']);
}
/**
* Returns the cart subtotal.
*
* @return float
*/
public function getCartSubtotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (!isset($cart->cartPrices['basePrice']))
{
return 0;
}
@include_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/currencydisplay.php';
if (!class_exists('CurrencyDisplay'))
{
return 0;
}
$currency = \CurrencyDisplay::getInstance();
return $currency->roundByPriceConfig($cart->cartPrices['basePrice']);
}
/**
* Returns the shipping total.
*
* @return float
*/
protected function getShippingTotal()
{
if (!$cart = $this->getCart())
{
return 0;
}
if (!isset($cart->cartPrices['shipmentValue']) || !isset($cart->cartPrices['shipmentTax']))
{
return 0;
}
return $cart->cartPrices['shipmentValue'] + $cart->cartPrices['shipmentTax'];
}
} Conditions/Conditions/Component/VirtueMartCartContainsXProducts.php 0000644 00000001655 15235314577 0021753 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCartContainsXProducts extends VirtueMartBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['virtuemart.contains_x_products'];
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->options->get('selection');
}
public function value()
{
if (!$cartProducts = $this->getCartProducts())
{
return false;
}
return count($cartProducts);
}
} Conditions/Conditions/Component/J2StoreBase.php 0000644 00000003010 15235314577 0015532 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class J2StoreBase extends ComponentBase
{
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_j2store';
/**
* Indicates whether the page is a category page
*
* @return boolean
*/
protected function isCategory()
{
return is_null($this->request->task);
}
/**
* Indicates whether the page is a single page
*
* @return boolean
*/
public function isSinglePage()
{
return (in_array($this->request->view, ['products', 'producttags']) && $this->request->task == 'view');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
// Get product information
require_once JPATH_ADMINISTRATOR . '/components/com_j2store/helpers/product.php';
// Make sure J2Store is loaded
if (!class_exists('J2Product'))
{
return;
}
$item = \J2Product::getInstance()->setId($this->request->id)->getProduct();
if (!is_object($item) || !isset($item->source))
{
return;
}
return $item->source->catid;
}
} Conditions/Conditions/Component/JBusinessDirectoryBusinessCategory.php 0000644 00000001321 15235314577 0022456 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryBusinessCategory extends JBusinessDirectoryBusinessBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbusinessdirectory.business_category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('jbusinessdirectory_categories', 'parent_id');
}
} Conditions/Conditions/Component/HikashopCartContainsXProducts.php 0000644 00000001635 15235314577 0021415 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCartContainsXProducts extends HikashopBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['hikashop.cart_contains_x_products'];
public function prepareSelection()
{
if ($this->operator === 'range')
{
return [
'value1' => (float) $this->options->get('selection'),
'value2' => (float) $this->options->get('params.value2', false)
];
}
return (float) $this->selection;
}
public function value()
{
if (!$cartProducts = $this->getCartProducts())
{
return false;
}
return count($cartProducts);
}
} Conditions/Conditions/Component/HikashopCurrentProductStock.php 0000644 00000000763 15235314577 0021141 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopCurrentProductStock extends HikashopBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCurrentProductStock();
}
} Conditions/Conditions/Component/ContentBase.php 0000644 00000005522 15235314577 0015666 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class ContentBase extends ComponentBase
{
/**
* Get single page's assosiated categories
*
* @param integer The Single Page id
*
* @return integer
*/
protected function getSinglePageCategories($id)
{
// If the article is not assigned to any menu item, the cat id should be available in the query string. Let's check it.
if ($requestCatID = $this->app->input->getInt('catid', null))
{
return $requestCatID;
}
// Apparently, the catid is not available in the Query String. Let's ask Article model.
$item = $this->getItem($id);
if (is_object($item) && isset($item->catid))
{
return $item->catid;
}
}
/**
* Load a Joomla article data object.
*
* @return object
*/
public function getItem($id = null)
{
$id = is_null($id) ? $this->request->id : $id;
// Sanity check
if (is_null($id))
{
return;
}
$hash = md5('contentItem' . $id);
$cache = $this->factory->getCache();
if ($cache->has($hash))
{
return $cache->get($hash);
}
// Prevent "Article not found" error on J3.
if (!defined('nrJ4'))
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $db->q((int) $id));
$db->setQuery($query);
if (!$db->loadResult())
{
return $cache->set($hash, null);
}
}
// Use try catch to prevent fatal errors in case the article is not found
try
{
$model = $this->getArticleModel();
$item = $model->getItem($id);
if ($item)
{
$item->images = is_string($item->images) ? json_decode($item->images) : $item->images;
$item->urls = is_string($item->urls) ? json_decode($item->urls) : $item->urls;
$item->attribs = is_string($item->attribs) ? json_decode($item->attribs) : $item->attribs;
}
return $cache->set($hash, $item);
} catch (\Throwable $th)
{
return null;
}
}
/**
* Return the Article's model.
*
* @return object
*/
private function getArticleModel()
{
if (defined('nrJ4'))
{
$mvcFactory = Factory::getApplication()->bootComponent('com_content')->getMVCFactory();
return $mvcFactory->createModel('Article', 'Administrator');
}
// Joomla 3
BaseDatabaseModel::addIncludePath(JPATH_SITE . '/components/com_content/models');
return BaseDatabaseModel::getInstance('Article', 'ContentModel');
}
} Conditions/Conditions/Component/DJEventsBase.php 0000644 00000002065 15235314577 0015735 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class DJEventsBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_djevents';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('cat_id'))
->from('#__djev_events')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/JEventsBase.php 0000644 00000003201 15235314577 0015622 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JEventsBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'icalrepeat';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_jevents';
/**
* Indicates whether the page is a single page
*
* @return boolean
*/
public function isSinglePage()
{
return ($this->request->task == 'icalrepeat.detail' || $this->request->task == 'icalrepeat');
}
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->id = $this->app->input->get('evid');
$this->request->task = $this->app->input->get('jevtask');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('catid')
->from('#__jevents_vevent')
->where($db->quoteName('ev_id') . '=' . $id);
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/JBusinessDirectoryOfferSingle.php 0000644 00000001455 15235314577 0021400 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryOfferSingle extends JBusinessDirectoryOfferBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jbuisnessdirectory.offer_single'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/K2Item.php 0000644 00000004106 15235314577 0014551 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
use NRFramework\Functions;
defined('_JEXEC') or die;
class K2Item extends K2Base
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['k2_items', 'k2item'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
$pass = $this->passSinglePage();
// Keywords Checking
$contentKeywords = $this->params->get('cont_keywords', '');
$metaKeywords = $this->params->get('meta_keywords', '');
// If both are empty, do not maky any further check
if (empty($contentKeywords) && empty($metaKeywords))
{
return $pass;
}
// Load current K2 Item object
if (!$item = $this->getK2Item())
{
return false;
}
// check items's text
if (!empty($contentKeywords))
{
$pass = $this->passArrayInString($contentKeywords, $item->introtext . $item->fulltext);
}
// check item's metakeywords
if (!empty($metaKeywords))
{
$pass = $this->passArrayInString($metaKeywords, $item->metakey);
}
return $pass;
}
/**
* Returns the assignment's value
*
* @return int Article ID
*/
public function value()
{
return $this->request->id;
}
/**
* Checks if an array of values (needle) exists in a text (haystack).
*
* @param array $needle The searched array of values.
* @param string $haystack The text
*
* @return bool
*/
private function passArrayInString($needle, $haystack)
{
if (empty($needle) || empty($haystack))
{
return false;
}
$needle = Functions::makeArray($needle);
return \NRFramework\Functions::strpos_arr($needle, $haystack);
}
} Conditions/Conditions/Component/RSBlogSingle.php 0000644 00000001373 15235314577 0015753 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSBlogSingle extends RSBlogBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['rsblog.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/HikashopPurchasedProduct.php 0000644 00000003162 15235314577 0020425 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class HikashopPurchasedProduct extends HikashopBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
if (!is_array($this->selection) || empty($this->selection))
{
return;
}
return $this->hasPurchased($this->selection);
}
/**
* Returns which given products has the current logged-in user purchased.
*
* @param array $product_ids
*
* @return array
*/
private function hasPurchased($product_ids = [])
{
if (!$product_ids)
{
return;
}
if (!$user = $this->factory->getUser())
{
return;
}
if (!$user->id)
{
return;
}
$query = $this->db->getQuery(true)
->clear()
->select('DISTINCT op.order_id')
->from('#__hikashop_order_product AS op')
->leftJoin('#__hikashop_order AS o ON o.order_id = op.order_id')
->leftJoin('#__hikashop_user AS u ON u.user_id = o.order_user_id')
->where('op.product_id IN (' . implode(',', $product_ids) . ')')
->where('o.order_status IN ("confirmed", "shipped")')
->where('u.user_cms_id = ' . (int) $user->id);
$this->db->setQuery($query);
return $this->db->loadColumn();
}
} Conditions/Conditions/Component/JShoppingCategory.php 0000644 00000002230 15235314577 0017051 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JShoppingCategory extends JShoppingBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['jshopping.catagory'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('jshopping_categories', 'category_parent_id');
}
/**
* Returns all parent rows
*
* @param integer $id Row primary key
* @param string $table Table name
* @param string $parent Parent column name
* @param string $child Child column name
*
* @return array Array with IDs
*/
public function getParentIds($id = 0, $table = 'jshopping_categories', $parent = 'category_parent_id', $child = 'category_id')
{
return parent::getParentIds($id, $table, $parent, $child);
}
} Conditions/Conditions/Component/GridboxCategory.php 0000644 00000001212 15235314577 0016545 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class GridboxCategory extends GridboxBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['gridbox.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('gridbox_categories', 'parent');
}
} Conditions/Conditions/Component/EshopBase.php 0000644 00000002100 15235314577 0015317 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EshopBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'product';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_eshop';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__eshop_productcategories')
->where($db->quoteName('product_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/ComponentBase.php 0000644 00000013261 15235314577 0016215 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use NRFramework\Functions;
/**
* Base class used by component-based assignments. Class properties defaults to com_content.
*/
abstract class ComponentBase extends Condition
{
/**
* The component's Category Page view name
*
* @var string
*/
protected $viewCategory = 'category';
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'article';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_content';
/**
* Request information
*
* @var mixed
*/
protected $request = null;
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options = null, $factory = null)
{
parent::__construct($options, $factory);
$request = new \stdClass;
$request->view = $this->app->input->get('view');
$request->task = $this->app->input->get('task');
$request->option = $this->app->input->get('option');
$request->layout = $this->app->input->get('layout');
$request->id = $this->app->input->getInt('id');
// Check if request is forwarded
if ($context = $this->app->input->get('forward_context'))
{
if (isset($context['request']))
{
$request = (object) $context['request'];
}
}
$this->request = $request;
}
/**
* Returns the assignment's value
*
* @return array Category IDs
*/
public function value()
{
return $this->getCategoryIds();
}
/**
* Indicates whether the current view concerns a Category view
*
* @return boolean
*/
protected function isCategoryPage()
{
return ($this->request->view == $this->viewCategory);
}
/**
* Indicates whether the current view concerncs a Single Page view
*
* @return boolean
*/
public function isSinglePage()
{
return ($this->request->view == $this->viewSingle);
}
/**
* Check if we are in the right context and we're manipulating the correct component
*
* @return bool
*/
protected function passContext()
{
return ($this->request->option == $this->component_option);
}
/**
* Returns category IDs based
*
* @return array
*/
protected function getCategoryIDs()
{
$id = $this->request->id;
// Make sure we have an ID.
if (empty($id))
{
return;
}
// If this is a Category page, return the Category ID from the Query String
if ($this->isCategoryPage())
{
return (array) $id;
}
// If this is a Single Page, return all assosiated Category IDs.
if ($this->isSinglePage())
{
return $this->getSinglePageCategories($id);
}
}
/**
* Checks whether the current page is within the selected categories
*
* @param string $ref_table The referenced table
* @param string $ref_parent_column The name of the parent column in the referenced table
*
* @return boolean
*/
protected function passCategories($ref_table = 'categories', $ref_parent_column = 'parent_id')
{
if (empty($this->selection) || !$this->passContext())
{
return false;
}
// Include Children switch: 0 = No, 1 = Yes, 2 = Child Only
$inc_children = $this->params->get('inc_children');
// Setup supported views
$view_single = $this->params->get('view_single', true);
$view_category = $this->params->get('view_category', false);
// Check if we are in a valid context
if (!($view_category && $this->isCategoryPage()) && !($view_single && $this->isSinglePage()))
{
return false;
}
// Start Checks
$pass = false;
// Get current page assosiated category IDs. It can be a single ID of the current Category view or multiple IDs assosiated to active item.
$catids = $this->getCategoryIDs();
$catids = is_array($catids) ? $catids : (array) $catids;
foreach ($catids as $catid)
{
$pass = in_array($catid, $this->selection);
if ($pass)
{
// If inc_children is either disabled or set to 'Also on Childs', there's no need for further checks.
// The condition is already passed.
if (in_array($this->params->get('inc_children'), [0, 1]))
{
break;
}
// We are here because we need childs only. Disable pass and continue checking parent IDs.
$pass = false;
}
// Pass check for child items
if (!$pass && $this->params->get('inc_children'))
{
$parent_ids = $this->getParentIDs($catid, $ref_table, $ref_parent_column);
foreach ($parent_ids as $id)
{
if (in_array($id, $this->selection))
{
$pass = true;
break 2;
}
}
unset($parent_ids);
}
}
return $pass;
}
/**
* Check whether this page passes the validation
*
* @return void
*/
protected function passSinglePage()
{
// Make sure we are in the right context
if (empty($this->selection) || !$this->passContext() || !$this->isSinglePage())
{
return false;
}
if (!is_array($this->selection))
{
$this->selection = Functions::makeArray($this->selection);
}
return parent::pass();
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
* @return array
*/
abstract protected function getSinglePageCategories($id);
} Conditions/Conditions/Component/JBusinessDirectoryEventBase.php 0000644 00000002447 15235314577 0021053 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryEventBase extends JBusinessDirectoryBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options = null, $factory = null)
{
parent::__construct($options, $factory);
$this->request->id = (int) $this->app->input->getInt('eventId');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('categoryId'))
->from('#__jbusinessdirectory_company_event_category')
->where($db->quoteName('eventId') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/EventBookingBase.php 0000644 00000002110 15235314577 0016634 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EventBookingBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'event';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_eventbooking';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('category_id')
->from('#__eb_event_categories')
->where($db->quoteName('event_id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/QuixBase.php 0000644 00000002024 15235314577 0015174 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class QuixBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'page';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_quix';
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('catid')
->from('#__quix')
->where($db->quoteName('id') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/JBusinessDirectoryBusinessBase.php 0000644 00000001264 15235314577 0021561 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryBusinessBase extends JBusinessDirectoryBase
{
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options = null, $factory = null)
{
parent::__construct($options, $factory);
$this->request->id = (int) $this->app->input->getInt('companyId');
}
} Conditions/Conditions/Component/VirtueMartCurrentProductStock.php 0000644 00000000767 15235314577 0021501 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class VirtueMartCurrentProductStock extends VirtueMartBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCurrentProductStock();
}
} Conditions/Conditions/Component/JBusinessDirectoryOfferBase.php 0000644 00000002447 15235314577 0021033 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class JBusinessDirectoryOfferBase extends JBusinessDirectoryBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'offer';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options = null, $factory = null)
{
parent::__construct($options, $factory);
$this->request->id = (int) $this->app->input->getInt('offerId');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('categoryId'))
->from('#__jbusinessdirectory_company_offer_category')
->where($db->quoteName('offerId') . '=' . $db->q($id));
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/VirtueMartPurchasedProduct.php 0000644 00000003145 15235314577 0020762 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
Use Joomla\CMS\Factory;
class VirtueMartPurchasedProduct extends VirtueMartBase
{
/**
* Pass check
*
* @return bool
*/
public function pass()
{
if (!is_array($this->selection) || empty($this->selection))
{
return;
}
return $this->hasPurchased($this->selection);
}
/**
* Returns which given products has the current logged-in user purchased.
*
* @param array $product_ids
*
* @return array
*/
private function hasPurchased($product_ids = [])
{
if (!$product_ids)
{
return;
}
if (!$user = Factory::getUser())
{
return;
}
if (!$user->id)
{
return;
}
$query = $this->db->getQuery(true)
->clear()
->select('DISTINCT o.virtuemart_order_id')
->from('#__virtuemart_orders AS o')
->leftJoin('#__virtuemart_order_items AS oi ON oi.virtuemart_order_id = o.virtuemart_order_id')
->where('oi.virtuemart_product_id IN (' . implode(',', $product_ids) . ')')
->where('o.order_status IN ("C", "S", "F")')
->where('o.virtuemart_user_id = ' . (int) $user->id);
$this->db->setQuery($query);
return $this->db->loadColumn();
}
} Conditions/Conditions/Component/SobiProBase.php 0000644 00000003360 15235314577 0015627 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class SobiProBase extends ComponentBase
{
/**
* The component's Single Page view name
*
* @var string
*/
protected $viewSingle = 'entry';
/**
* The component's option name
*
* @var string
*/
protected $component_option = 'com_sobipro';
/**
* Class Constructor
*
* @param object $options
* @param object $factory
*/
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->request->view = 'entry';
// Make sure SPRequest is loaded
if (!class_exists('SPRequest'))
{
return;
}
$this->request->id = (int) \SPRequest::sid();
}
/**
* Indicates whether the page is a single page
*
* @return boolean
*/
public function isSinglePage()
{
return (parent::isSinglePage() && $this->request->task == 'entry.details');
}
/**
* Get single page's assosiated categories
*
* @param Integer The Single Page id
*
* @return array
*/
protected function getSinglePageCategories($id)
{
$db = $this->db;
$query = $db->getQuery(true)
->select('pid')
->from('#__sobipro_relations')
->where($db->quoteName('id') . '=' . $db->q($id))
->where($db->quoteName('oType') . " = 'entry'");
$db->setQuery($query);
return $db->loadColumn();
}
} Conditions/Conditions/Component/EventBookingSingle.php 0000644 00000001412 15235314577 0017207 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class EventBookingSingle extends EventBookingBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['eventbookingsingle'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passSinglePage();
}
/**
* Returns the assignment's value
*
* @return int
*/
public function value()
{
return $this->request->id;
}
} Conditions/Conditions/Component/RSEventsProCategory.php 0000644 00000001221 15235314577 0017341 0 ustar 00 <?php
/**
* @author Tassos.gr
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Component;
defined('_JEXEC') or die;
class RSEventsProCategory extends RSEventsProBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['rseventspro.category'];
/**
* Pass check
*
* @return bool
*/
public function pass()
{
return $this->passCategories('categories', 'parent_id');
}
} Conditions/Conditions/Date/DateBase.php 0000644 00000004351 15235314577 0014043 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class DateBase extends Condition
{
/**
* Server's Timezone
*
* @var DateTimeZone
*/
protected $tz;
/**
* If set to True, dates will be constructed with modified offset based on the passed timezone
*
* @var Boolean
*/
protected $modify_offset = true;
/**
* Class constructor
*
* @param object $assignment
*/
public function __construct($assignment = null, $factory = null)
{
parent::__construct($assignment, $factory);
// Set timezone
if ($timezone = $this->params->get('timezone'))
{
$this->tz = new \DateTimeZone($timezone);
}
else
{
$this->tz = new \DateTimeZone($this->app->getCfg('offset', 'GMT'));
}
// Set modify offset switch
$this->modify_offset = $this->params->get('modify_offset', true);
// Set now date
$now = $this->params->get('now', 'now');
$this->date = $this->getDate($now);
}
/**
* Checks if the current datetime is between the specified range
*
* @param JDate &$up_date
* @param JDate &$down_date
*
* @return bool
*/
protected function checkRange(&$up_date, &$down_date)
{
if (!$up_date && !$down_date)
{
return false;
}
$now = $this->date->getTimestamp();
if (((bool)$up_date && $up_date->getTimestamp() > $now) ||
((bool)$down_date && $down_date->getTimestamp() < $now))
{
return false;
}
return true;
}
/**
* Create a date object based on the given string and apply timezone.
*
* @param String $date
*
* @return void
*/
protected function getDate($date = 'now')
{
// Fix the date string
\NRFramework\Functions::fixDate($date);
if ($this->modify_offset)
{
// Create date, set timezone and modify offset
$date = $this->factory->getDate($date)->setTimeZone($this->tz);
} else
{
// Create date and set timezone without modifyig offset
$date = $this->factory->getDate($date, $this->tz);
}
return $date;
}
} Conditions/Conditions/Date/Date.php 0000644 00000003007 15235314577 0013245 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
class Date extends DateBase
{
/**
* Checks if current date passes the given date range.
* Dates must be always passed in format: Y-m-d H:i:s
*
* @return bool
*/
public function pass()
{
$publish_up = $this->params->get('publish_up');
$publish_down = $this->params->get('publish_down');
// No valid dates
if (!$publish_up && !$publish_up)
{
return false;
}
$up = $publish_up ? $this->getDate($publish_up) : null;
$down = $publish_down ? $this->getDate($publish_down) : null;
return $this->checkRange($up, $down);
}
/**
* Returns the assignment's value
*
* @return \Date Current date
*/
public function value()
{
return $this->date;
}
/**
* This method is called before the value of the condition is stored into the database.
*
* Dates should be always stored in the database in GMT. Thus, we remove the timezone offset from the date.
*
* @param array $rule The condition object.
*
* @return void
*/
public function onBeforeSave(&$rule)
{
\NRFramework\Functions::fixDateOffset($rule['params']['publish_up']);
\NRFramework\Functions::fixDateOffset($rule['params']['publish_down']);
}
} Conditions/Conditions/Date/Day.php 0000644 00000003176 15235314577 0013114 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
class Day extends DateBase
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['weekday'];
/**
* Cover special cases where the user checks whether the current day is a Weekday or Weekend.
*
* @param mixed $selection The current selection
*
* @return array
*/
public function prepareSelection()
{
$selection = (array) $this->getSelection();
foreach ($selection as $str)
{
$str = strtolower($str ?? '');
if (strpos($str, 'weekday') !== false)
{
$selection = array_merge($selection, range(1, 5));
continue;
}
if (strpos($str, 'weekend') !== false)
{
$selection = array_merge($selection, [6, 7]);
}
}
return $selection;
}
/**
* Return a list with all different formats of the current day.
*
* This returns the day in non-localized strings.
*
* @return array
*/
public function value()
{
return [
$this->date->format('l', true, false), // 'Friday'
$this->date->format('D', true, false), // 'Fri'
$this->date->format('N', true, false), // '1' (Monday) to '7' (Sunday)
];
}
} Conditions/Conditions/Date/Scheduler.php 0000644 00000012432 15235314577 0014310 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
/**
* DateTime Assignment Scheduling helper
*/
class Scheduler
{
/**
* Starting date
*
* @var object \DateTime
*/
protected $start_date;
/**
* Ending date
*
* @var object \DateTime
*/
protected $end_date;
/**
* Date to test against
*
* @var object \DateTime
*/
protected $current_date;
/**
* @var string
*/
protected $repetitionFrequency;
/**
* @var int
*/
protected $repetitionStep;
/**
* Used by 'weekly' repetition frequency
*
* @var array
*/
protected $weekdays;
/**
* The interval between the starting and current date
* http://php.net/manual/en/class.dateinterval.php
*
* @var object \DateInterval
*/
protected $interval;
/**
* Scheduler constructor
*
* @param array $options: Scheduling options
* start_date:
* end_date:
* current_date:
* repetitionFrequency: one of 'daily', 'weekly', 'monthly', 'yearly'
* repetitionStep: integer (1,2,3,...)
* weekdays: Array, used with 'weekly' repetitionFrequency, any of 'Monday', 'Tuesday', etc.
* Defaults to start_date's day name if empty
*/
public function __construct($options)
{
$this->start_date = $options['start_date'];
$this->end_date = array_key_exists('end_date', $options) ? $options['end_date'] : null;
$this->current_date = $options['current_date'];
$this->repetitionFrequency = $options['repetitionFrequency'];
$this->repetitionStep = $options['repetitionStep'];
$this->weekdays = array_key_exists('weekdays', $options) ?
array_map('ucfirst', $options['weekdays']) :
null;
//create a DateInterval object from current and start dates
$this->interval = $this->start_date->diff($this->current_date);
}
/**
* @return bool
*/
public function repeat()
{
//check if we are within the start/end date range (inclusive)
if (!$this->checkDateRange())
{
return false;
}
$result = false;
//
switch ($this->repetitionFrequency)
{
case 'daily':
$result = $this->repeatDaily();
break;
case 'weekly':
$result = $this->repeatWeekly();
break;
case 'monthly':
$result = $this->repeatMonthly();
break;
case 'yearly':
$result = $this->repeatYearly();
break;
}
return $result;
}
/**
* Daily repetition check
*
* @return bool
*/
protected function repeatDaily()
{
//get the number of days that have passed since start_date
$num_days = $this->interval->days;
if ($num_days % $this->repetitionStep !== 0) {
return false;
}
return true;
}
/**
* Weekly repetition check
*
* @return bool
*/
protected function repeatWeekly()
{
//get current_date's day name
$today_name = $this->current_date->format('l');
// if $this->weekdays is empty use start_date's day name
if (empty($this->weekdays))
{
$start_day_name = $this->start_date->format('l');
if ($start_day_name !== $today_name)
{
return false;
}
}
else
{
if (!in_array($today_name, $this->weekdays))
{
return false;
}
}
//get the number of weeks that passed since start_date
$num_weeks = floor($this->interval->days / 7);
if ($num_weeks % $this->repetitionStep !== 0)
{
return false;
}
return true;
}
/**
* Monthly repetition check
*
* @return bool
*/
protected function repeatMonthly()
{
//check if we are on the same day of the month
$start_day = $this->start_date->format('d');
$current_day = $this->current_date->format('d');
if ($start_day !== $current_day)
{
return false;
}
//get the number of months that have passed since start_date
$num_months = ($this->interval->y * 12) + $interval->m;
if ($num_months % $this->repetitionStep !== 0)
{
return false;
}
return true;
}
/**
* Yearly repetition check
*
* @return bool
*/
protected function repeatYearly()
{
//check if we are on the same month and day
$start_day_month = $this->start_date->format('d-m');
$current_day_month = $this->current_date->format('d-m');
if ($start_day_month !== $current_day_month)
{
return false;
}
//get the number of years that have passed since start_date
$num_years = $this->interval->y;
if ($num_years % $this->repetitionStep !== 0)
{
return false;
}
return true;
}
/**
* Checks if the current date is between the start/end range (inclusive)
*
* @return bool
*/
protected function checkDateRange()
{
if ($this->start_date > $this->current_date)
{
return false;
}
if (!empty($this->end_date) && $this->end_date < $this->current_date)
{
return false;
}
return true;
}
} Conditions/Conditions/Date/Time.php 0000644 00000002725 15235314577 0013274 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class Time extends DateBase
{
/**
* If set to True, dates will be constructed with modified offset based on the passed timezone
*
* @var Boolean
*/
protected $modify_offset = false;
/**
* Checks if current time passes the given time range
*
* @return bool
*/
public function pass()
{
$up = $this->date->format('Y-m-d', true) . ' ' . $this->params->get('publish_up');
$down = $this->date->format('Y-m-d', true) . ' ' . $this->params->get('publish_down');
$up = $this->factory->getDate((string) $up, $this->tz);
$down = $this->factory->getDate((string) $down, $this->tz);
return $this->checkRange($up, $down);
}
/**
* Returns the assignment's value
*
* @return \Date Current date
*/
public function value()
{
return $this->date;
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_' . strtoupper($this->getName()), $this->date->format('H:i', true));
}
} Conditions/Conditions/Date/Month.php 0000644 00000001424 15235314577 0013456 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Date;
defined('_JEXEC') or die;
class Month extends DateBase
{
/**
* Returns the assignment's value
*
* This returns the month in non-localized strings.
*
* @return string Name of the current month
*/
public function value()
{
return [
$this->date->format('F', true, false),
$this->date->format('M', true, false),
$this->date->format('n', true, false),
$this->date->format('m', true, false),
];
}
} Conditions/Conditions/Joomla/Menu.php 0000644 00000004403 15235314577 0013641 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Menu extends Condition
{
protected $itemID = null;
public function __construct($options, $factory)
{
parent::__construct($options, $factory);
$this->itemID = $this->app->input->getInt('Itemid', 0);
$this->selection = (array) $this->selection;
}
/**
* Pass check for menu items
*
* @return bool
*/
public function pass()
{
$includeChildren = $this->params->get('inc_children', false); // includeChildren is more user-friendly for Restrict Content
$includeNoItemID = $this->params->get('noitem', false);
// Pass if selection is empty or the itemid is missing
if (!$this->itemID || empty($this->selection))
{
return $includeNoItemID;
}
// return true if menu type is in selection
$menutype = 'type.' . $this->getMenuType();
if (in_array($menutype, $this->selection))
{
return true;
}
// return true if menu is in selection and we are not including child items only
if (in_array($this->itemID, $this->selection))
{
return ($includeChildren != 2);
}
// Let's discover child items.
// Obviously if the option is disabled return false.
if (!$includeChildren)
{
return false;
}
// Get menu item parents
$parent_ids = $this->getParentIds($this->itemID);
$parent_ids = array_diff($parent_ids, array('1'));
foreach ($parent_ids as $id)
{
if (!in_array($id, $this->selection))
{
continue;
}
return true;
}
return false;
}
/**
* Returns the assignment's value
*
* @return integer Menu ID
*/
public function value()
{
return $this->itemID;
}
/**
* Get active menu items's menu type
*
* @return bool False on failure, string on success
*/
private function getMenuType()
{
if (empty($this->itemID))
{
return;
}
$menu = $this->app->getMenu()->getItem((int) $this->itemID);
return isset($menu->menutype) ? $menu->menutype : false;
}
} Conditions/Conditions/Joomla/UserID.php 0000644 00000001222 15235314577 0014064 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class UserID extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['user.id'];
/**
* Returns the assignment's value
*
* @return int User ID
*/
public function value()
{
return $this->user->id;
}
} Conditions/Conditions/Joomla/Component.php 0000644 00000001127 15235314577 0014677 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Component extends Condition
{
/**
* Returns the assignment's value
*
* @return string The component's name
*/
public function value()
{
return $this->app->input->get('option');
}
} Conditions/Conditions/Joomla/UserGroup.php 0000644 00000003564 15235314577 0014677 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Language\Text;
class UserGroup extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['user.group'];
/**
* Returns the ID and the Title of the user's authorized groups
*
* @return array User groups
*/
public function value()
{
$groups = $this->user->getAuthorisedGroups();
// Beyond the IDs return also the Titles of the User Groups but only when the User Value includes Titles (Performance-wise). This is mainly used in conditional shortcode to be able to do comparison with Titles.
if ($this->selection)
{
$userValueHasTitles = array_filter((array) $this->selection, function($item)
{
return !is_numeric($item);
});
if ($userValueHasTitles)
{
foreach ($groups as $id)
{
$groups[] = Access::getGroupTitle($id);
}
}
}
return $groups;
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->qn('title'))
->from('#__usergroups')
->where($db->qn('id') . ' IN ' . '(' . implode(',', $this->user->getAuthorisedGroups()) . ')');
$db->setQuery($query);
$value = implode(', ', $db->loadColumn());
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_' . strtoupper($this->getName()), $value);
}
} Conditions/Conditions/Joomla/Language.php 0000644 00000001243 15235314577 0014457 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
class Language extends Condition
{
/**
* Returns the assignment's value
*
* @return array Language strings
*/
public function value()
{
$lang = $this->factory->getLanguage();
$lang_strings = $lang->getLocale();
$lang_strings[] = $lang->getTag();
return $lang_strings;
}
} Conditions/Conditions/Joomla/AccessLevel.php 0000644 00000004465 15235314577 0015136 0 ustar 00 <?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Conditions\Conditions\Joomla;
defined('_JEXEC') or die;
use NRFramework\Conditions\Condition;
use NRFramework\Cache;
use Joomla\CMS\Language\Text;
class AccessLevel extends Condition
{
/**
* Shortcode aliases for this Condition
*/
public static $shortcode_aliases = ['user.access'];
/**
* Get the user's authorized view levels
*
* @return array User groups
*/
public function value()
{
$viewLevels = $this->user->getAuthorisedViewLevels();
// Beyond the IDs return also the Titles of the User Access Levels but only when the User Value includes Titles (Performance-wise). This is mainly used in conditional shortcode to be able to do comparison with Titles.
if ($this->selection)
{
$userValueHasTitles = array_filter((array) $this->selection, function($item)
{
return !is_numeric($item);
});
if ($userValueHasTitles)
{
$viewLevels = array_merge($viewLevels, $this->getAuthorisedViewLevelTitles());
}
}
return $viewLevels;
}
/**
* A one-line text that describes the current value detected by the rule. Eg: The current time is %s.
*
* @return string
*/
public function getValueHint()
{
return Text::sprintf('NR_DISPLAY_CONDITIONS_HINT_' . strtoupper($this->getName()), implode(', ', $this->getAuthorisedViewLevelTitles()));
}
/**
* Return a list with user access level titles
*
* @return array
*/
private function getAuthorisedViewLevelTitles()
{
$callback = function()
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->qn('title'))
->from('#__viewlevels')
->where($db->qn('id') . ' IN ' . '(' . implode(',', $this->user->getAuthorisedViewLevels()) . ')');
$db->setQuery($query);
return $db->loadColumn();
};
return Cache::memo('getAuthorisedViewLevelTitles', $callback);
}
} Notices/Helper.php 0000644 00000002375 15235314577 0010123 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
* @credits https://github.com/codeigniter4/CodeIgniter4/blob/develop/app/Config/Mimes.php
*/
namespace NRFramework\Notices;
// No direct access
defined('_JEXEC') or die;
use \NRFramework\Extension;
class Helper
{
public static function getRemoteNoticesData()
{
}
/**
* Returns the extension details for given element.
*
* @param array $data
* @param string $element
*
* @return array
*/
public static function getExtensionDetails($data, $element)
{
// Return bundle only if its active
if (isset($data['bundle']) && $data['bundle']['active'])
{
return $data['bundle'];
}
$alias = Extension::getExtensionDataFileAlias($element);
// If no license data found for this extension
if (!isset($data[$alias]))
{
// Return the expired bundle information if it exists
if (isset($data['bundle']))
{
return $data['bundle'];
}
// No bundle exists, return nothing
return;
}
// Return the extension's license data details
return $data[$alias];
}
} Notices/Notices.php 0000644 00000015636 15235314577 0010314 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
* @credits https://github.com/codeigniter4/CodeIgniter4/blob/develop/app/Config/Mimes.php
*/
namespace NRFramework\Notices;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Factory;
use \NRFramework\Extension;
class Notices
{
/**
* The payload.
*
* @var array
*/
private $payload;
/**
* The extension's ext_element we are showing notices.
*
* Example: acf
*
* @var string
*/
private $ext_element;
/**
* The extension's main XML file location folder.
*
* Example: plg_system_acf, com_rstbox
*
* @var string
*/
private $ext_xml;
/**
* The extension type.
*
* Example: plugin, component, module, etc...
*
* @var string
*/
private $ext_type = 'component';
/**
* The notices to exclude.
*
* @var array
*/
private $exclude = [];
/**
* Define how old (in days) the file that holds all extensions data needs to be set as expired,
* so we can fetch new data.
*
* @var int
*/
private $extensions_data_file_days_old = 1;
/**
* Download Key.
*
* @var String
*/
protected $download_key = null;
/**
* The license data for the given download key.
*
* @var array
*/
protected $license_data = [];
/**
* Notices Instance.
*
* @var Notices
*/
private static $instance;
public function __construct($payload = [])
{
$this->payload = $payload;
$this->ext_element = isset($this->payload['ext_element']) ? $this->payload['ext_element'] : '';
$this->ext_xml = isset($this->payload['ext_xml']) ? $this->payload['ext_xml'] : '';
$this->ext_type = isset($this->payload['ext_type']) ? $this->payload['ext_type'] : $this->ext_type;
$this->exclude = isset($this->payload['exclude']) ? $this->payload['exclude'] : [];
$this->download_key = \NRFramework\Functions::getDownloadKey();
}
/**
* Returns class instance
*
* @param array $payload
*
* @return object
*/
public static function getInstance($payload = [])
{
if (is_null(self::$instance))
{
self::$instance = new self($payload);
}
return self::$instance;
}
/**
* Show all available notices.
*
* @return void
*/
public function show()
{
// Show only for Super Users
if (!$this->isSuperUser())
{
return;
}
HTMLHelper::stylesheet('plg_system_nrframework/notices.css', ['relative' => true, 'version' => 'auto']);
HTMLHelper::script('plg_system_nrframework/notices.js', ['relative' => true, 'version' => 'auto']);
$payload = [
'ext_element' => $this->ext_element,
'ext_xml' => $this->ext_xml,
'ext_type' => $this->ext_type,
'exclude' => $this->exclude
];
echo LayoutHelper::render('notices/tmpl', $payload, dirname(dirname(__DIR__)) . '/layouts');
}
/**
* Check if the current user is a Super User.
*
* @return bool
*/
private function isSuperUser()
{
return Factory::getUser()->authorise('core.admin');
}
/**
* Returns the base notices.
*
* @param array $notices
*
* @return void
*/
private function getBaseNotices()
{
$base_notices = [
'Outdated',
'DownloadKey',
'Geolocation',
'UpgradeToPro',
'UpgradeToBundle'
];
// Exclude notices we should not display
if (count($this->exclude))
{
foreach ($base_notices as $key => $notice)
{
if (!in_array($notice, $this->exclude))
{
continue;
}
unset($base_notices[$key]);
}
}
$notices = [];
// Initialize notices
foreach ($base_notices as $key => $notice)
{
$class = '\NRFramework\Notices\Notices\\' . $notice;
// Skip empty notice
if (!$html = (new $class($this->payload))->render())
{
continue;
}
$notices[strtolower($notice)] = $html;
}
return $notices;
}
/**
* Returns which license-related notices to show.
*
* Notices:
* - Extension expires in date
* - Extension expired at date
*
* @return array
*/
private function getLicensesBasedNoticesToShow()
{
// If no data found for this extension, abort
if (!$extension_data = \NRFramework\Notices\Helper::getExtensionDetails($this->license_data, $this->ext_element))
{
return false;
}
if (!array_key_exists('active', $extension_data))
{
return;
}
$notices = [];
// Active subscription and we have a expiration date
if ($extension_data['active'] && array_key_exists('expires_in', $extension_data) && $extension_data['expires_in'])
{
$notices[] = (new Notices\Expiring(array_merge($this->payload, [
'expires_in' => $extension_data['expires_in'],
'plan' => $extension_data['plan']
])))->render();
}
/**
* We should not have an active subscription and the "expired_at" date must be set.
*
* If "active" is true and an "expired_at" date is set, it means we have a Bundle plan.
*/
if (!$extension_data['active'] && array_key_exists('expired_at', $extension_data) && $extension_data['expired_at'])
{
$notices[] = (new Notices\Expired(array_merge($this->payload, [
'expired_at' => $extension_data['expired_at'],
'plan' => $extension_data['plan']
])))->render();
}
if (!$notices)
{
return;
}
return implode('', $notices);
}
/**
* Returns the based notices:
*
* Notices:
* - Base notices
* - Outdated
* - Download Key
* - Geolocation
* - Upgrade To Pro
* - Upgrade To Bundle
* - Update notice
* - Extension expires in date
* - Extension expired at date
* - Rate (If none of the license-related notices appear)
*
* @return string
*/
public function getNotices()
{
// Check and Update the local licenses data
$this->checkAndUpdateExtensionsData();
$notices = $this->getBaseNotices();
// Show Update Notice
if ($update_html = (new Notices\Update($this->payload))->render())
{
$notices['update'] = $update_html;
}
if ($license_notices = $this->getLicensesBasedNoticesToShow())
{
$notices['license'] = $license_notices;
}
else if ($rate_html = (new Notices\Rate($this->payload))->render())
{
$notices['rate'] = $rate_html;
}
return $notices;
}
/**
* Checks whether the current extensions data has expired and updates the data file.
*
* Also checks and sets the installation date of the extension.
*
* @return bool
*/
public function checkAndUpdateExtensionsData()
{
// Sets licenses information
$this->license_data = \NRFramework\Helpers\License::getRemoteLicenseData($this->download_key);
// Add the license data to the payload as well
$this->payload['license_data'] = $this->license_data;
// Set installation date
Extension::setInstallationDate($this->ext_element, gmdate('Y-m-d H:i:s'));
}
} Notices/Notices/DownloadKey.php 0000644 00000006646 15235314577 0012535 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Functions;
class DownloadKey extends Notice
{
protected $notice_payload = [
'type' => 'error',
'class' => 'download-key',
'dismissible' => false,
'download_key' => null,
'state' => null
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['download_key'] = Functions::getDownloadKey();
$this->payload['state'] = isset($this->payload['license_data']['state']) ? $this->payload['license_data']['state'] : null;
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
$text = !empty($this->payload['download_key']) || ($this->payload['state'] && in_array($this->payload['state'], ['invalid_key'])) ? Text::_('NR_IS_INVALID') : Text::_('NR_IS_MISSING');
return sprintf(Text::_('NR_DOWNLOAD_KEY_TEXT'), $text);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
$text = !empty($this->payload['download_key']) || ($this->payload['state'] && in_array($this->payload['state'], ['invalid_key'])) ? Text::_('NR_A_VALID') : Text::_('NR_YOUR');
return sprintf(Text::_('NR_DOWNLOAD_KEY_MISSING_DESC'), $this->extension_name, $text);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = 'https://www.tassos.gr/kb/general/how-to-activate-your-pro-version';
return '<input type="text" class="tf-notice-download-key" value="' . htmlspecialchars($this->getDownloadKey()) . '" placeholder="' . Text::_('NR_ENTER_YOUR_DOWNLOAD_KEY') . '" />
<a href="#" class="tf-notice-download-key-btn tf-notice-btn info">' . Text::_('JAPPLY') . '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15" height="15" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" stroke="currentColor" stroke-width="8" r="38" stroke-dasharray="179.0707812546182 61.690260418206066">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
</circle>
</svg></a>
<a href="' . Functions::getUTMURL($url, 'UserNotice', 'DownloadKey') . '" target="_blank">' . Text::_('JHELP') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// Ensure customer is using the Pro version
if (!\NRFramework\Extension::isPro($this->payload['ext_xml']))
{
return false;
}
// If user is Pro but has no license details, show it
if (!$details = \NRFramework\Notices\Helper::getExtensionDetails($this->payload['license_data'], $this->payload['ext_element']))
{
return true;
}
// If state exists and key is invalid/or no subscriptions exist, return true
if ($this->payload['state'] && in_array($this->payload['state'], ['missing_key', 'invalid_key']))
{
return true;
}
if (!empty($this->getDownloadKey()))
{
return false;
}
return true;
}
private function getDownloadKey()
{
return isset($this->payload['download_key']) ? $this->payload['download_key'] : '';
}
} Notices/Notices/Expiring.php 0000644 00000004732 15235314577 0012074 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Functions;
class Expiring extends Notice
{
protected $notice_payload = [
'type' => 'warning',
'class' => 'expiring',
'expires_in' => '',
'plan' => ''
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['tooltip'] = Text::_('NR_NOTICE_EXPIRING_TOOLTIP');
$this->payload['expires_in'] = isset($this->payload['expires_in']) ? $this->payload['expires_in'] : false;
$this->payload['plan'] = isset($payload['plan']) ? $payload['plan'] : false;
}
/**
* Define the remaining days the subscription must have to display the expiring subscription notice.
*
* @var int
*/
private $expiring_notice_days = 30;
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_SUBSCRIPTION_EXPIRING'), $this->extension_name);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
$title = strtolower($this->payload['plan']) === 'bundle' ? $this->payload['plan'] : $this->extension_name . ' ' . $this->payload['plan'];
return sprintf(Text::_('NR_SUBSCRIPTION_EXPIRING_DESC'), $title, Functions::applySiteTimezoneToDate($this->payload['expires_in'], 'd M o'));
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = 'https://www.tassos.gr/subscriptions';
return '<a href="' . Functions::getUTMURL($url, 'UserNotice', 'SubscriptionExpiring') . '" target="_blank" class="tf-notice-btn info">' . sprintf(Text::_('NR_RENEW_X_PERCENT_OFF'), 30) . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, it's already hidden
if ($this->factory->getCookie('tfNoticeHideExpiringNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
// The date the extension expires.
if (!$this->payload['expires_in'])
{
return false;
}
// The days difference criteria must be met
if ($this->getDaysDifference(strtotime($this->payload['expires_in']), time()) > $this->expiring_notice_days)
{
return false;
}
return true;
}
} Notices/Notices/Update.php 0000644 00000004520 15235314577 0011524 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Extension;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Session\Session;
class Update extends Notice
{
protected $notice_payload = [
'type' => 'success',
'class' => 'update',
'current_version' => '',
'latest_version' => ''
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['current_version'] = Extension::getVersion($this->payload['ext_xml']);
$this->payload['latest_version'] = Extension::getLatestVersion($this->payload['ext_xml']);
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_EXTENSION_NEW_VERSION_IS_AVAILABLE'), $this->extension_name . ' v' . $this->payload['latest_version']);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return sprintf(Text::_('NR_EXTENSION_NOTICE_DESC'), $this->extension_name);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = Extension::getProductURL($this->payload['ext_xml']) . '/changelog';
return '<span class="orange-text text-bold">' . sprintf(Text::_('NR_YOUR_USING_VERSION'), $this->payload['current_version']) . '</span>
<a href="' . \NRFramework\Functions::getUTMURL($url, 'UserNotice', 'Update') . '" target="_blank" class="tf-notice-btn outline">' . Text::_('NR_VIEW_CHANGELOG') . '</a>
<a href="' . Uri::base() . 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1" class="tf-notice-btn success">' . Text::_('NR_UPDATE_NOW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, its been hidden
if ($this->factory->getCookie('tfNoticeHideUpdateNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
if (!$this->payload['latest_version'])
{
return false;
}
return version_compare($this->payload['latest_version'], $this->payload['current_version'], '>');
}
} Notices/Notices/Notice.php 0000644 00000023302 15235314577 0011522 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use \NRFramework\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
class Notice
{
/**
* The notice payload.
*
* @var array
*/
protected $notice_payload = [];
/**
* The payload.
*
* @var array
*/
protected $payload = [
/**
* The extension's element we are showing notices.
*
* Example: com_rstbox, plg_system_acf
*/
'ext_element' => '',
/**
* The extension's main XML file location folder.
*/
'ext_xml' => '',
/**
* The extension type.
*
* Example: component, plugin, module, etc...
*/
'ext_type' => 'component',
/**
* The notice type.
*/
'type' => '',
/**
* The notice icon.
*
* Inner part of the SVG icon.
*/
'icon' => '',
/**
* An array containing classes attached to the notice wrapper HTML Element.
*/
'class' => '',
/**
* Whether the notice is dismissible.
*/
'dismissible' => true,
/**
* The notice title.
*/
'title' => '',
/**
* The notice description.
*/
'description' => '',
/**
* The tooltip text explaining this action.
*/
'tooltip' => '',
/**
* The notice actions.
*/
'actions' => ''
];
/**
* The extension name.
*
* @var String
*/
protected $extension_name;
/**
* Factory.
*
* @var Factory
*/
protected $factory;
public function __construct($payload = [])
{
$this->payload = array_merge($this->payload, $this->notice_payload, $payload);
$this->factory = new \NRFramework\Factory();
$this->extension_name = Extension::getExtensionName($this->payload['ext_element']);
}
/**
* Renders notice.
*
* @return string
*/
public function render()
{
if (!$this->canRun())
{
return;
}
$this->prepare();
return LayoutHelper::render('notices/notice', $this->payload, dirname(dirname(dirname(__DIR__))) . '/layouts');
}
/**
* Prepares the notice.
*
* @return void
*/
private function prepare()
{
// Set title
if (method_exists($this, 'getTitle'))
{
$this->payload['title'] = $this->getTitle();
}
// Set description
if (method_exists($this, 'getDescription'))
{
$this->payload['description'] = $this->getDescription();
}
// Set actions
if (method_exists($this, 'getActions'))
{
$this->payload['actions'] = $this->getActions();
}
if (isset($this->payload['type']) && !empty($this->payload['type']))
{
// Set type of notice
$this->payload['class'] .= ' ' . $this->payload['type'];
// Set icon
switch ($this->payload['type'])
{
case 'warning':
$icon = '<mask id="mask0_105_19" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><rect width="40" height="40" fill="#D9D9D9"/></mask><g mask="url(#mask0_105_19)"><path d="M1.66669 35L20 3.33331L38.3334 35H1.66669ZM7.41669 31.6666H32.5834L20 9.99998L7.41669 31.6666ZM20 30C20.4722 30 20.8684 29.84 21.1884 29.52C21.5072 29.2011 21.6667 28.8055 21.6667 28.3333C21.6667 27.8611 21.5072 27.4655 21.1884 27.1466C20.8684 26.8266 20.4722 26.6666 20 26.6666C19.5278 26.6666 19.1322 26.8266 18.8134 27.1466C18.4934 27.4655 18.3334 27.8611 18.3334 28.3333C18.3334 28.8055 18.4934 29.2011 18.8134 29.52C19.1322 29.84 19.5278 30 20 30ZM18.3334 25H21.6667V16.6666H18.3334V25Z" fill="#F4B400"/></g>';
break;
case 'error':
$icon = '<mask id="mask0_105_7" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><rect width="40" height="40" fill="#D9D9D9"/></mask><g mask="url(#mask0_105_7)"><path d="M20.0001 28.3334C20.4723 28.3334 20.8684 28.1734 21.1884 27.8534C21.5073 27.5345 21.6668 27.139 21.6668 26.6668C21.6668 26.1946 21.5073 25.7984 21.1884 25.4784C20.8684 25.1595 20.4723 25.0001 20.0001 25.0001C19.5279 25.0001 19.1323 25.1595 18.8134 25.4784C18.4934 25.7984 18.3334 26.1946 18.3334 26.6668C18.3334 27.139 18.4934 27.5345 18.8134 27.8534C19.1323 28.1734 19.5279 28.3334 20.0001 28.3334ZM20.0001 21.6668C20.4723 21.6668 20.8684 21.5068 21.1884 21.1868C21.5073 20.8679 21.6668 20.4723 21.6668 20.0001V13.3334C21.6668 12.8612 21.5073 12.4651 21.1884 12.1451C20.8684 11.8262 20.4723 11.6668 20.0001 11.6668C19.5279 11.6668 19.1323 11.8262 18.8134 12.1451C18.4934 12.4651 18.3334 12.8612 18.3334 13.3334V20.0001C18.3334 20.4723 18.4934 20.8679 18.8134 21.1868C19.1323 21.5068 19.5279 21.6668 20.0001 21.6668ZM20.0001 36.6668C17.6945 36.6668 15.5279 36.229 13.5001 35.3534C11.4723 34.479 9.70844 33.2918 8.20844 31.7918C6.70844 30.2918 5.52121 28.5279 4.64677 26.5001C3.77121 24.4723 3.33344 22.3057 3.33344 20.0001C3.33344 17.6945 3.77121 15.5279 4.64677 13.5001C5.52121 11.4723 6.70844 9.70844 8.20844 8.20844C9.70844 6.70844 11.4723 5.52066 13.5001 4.6451C15.5279 3.77066 17.6945 3.33344 20.0001 3.33344C22.3057 3.33344 24.4723 3.77066 26.5001 4.6451C28.5279 5.52066 30.2918 6.70844 31.7918 8.20844C33.2918 9.70844 34.479 11.4723 35.3534 13.5001C36.229 15.5279 36.6668 17.6945 36.6668 20.0001C36.6668 22.3057 36.229 24.4723 35.3534 26.5001C34.479 28.5279 33.2918 30.2918 31.7918 31.7918C30.2918 33.2918 28.5279 34.479 26.5001 35.3534C24.4723 36.229 22.3057 36.6668 20.0001 36.6668ZM20.0001 33.3334C23.7223 33.3334 26.8751 32.0418 29.4584 29.4584C32.0418 26.8751 33.3334 23.7223 33.3334 20.0001C33.3334 16.2779 32.0418 13.1251 29.4584 10.5418C26.8751 7.95844 23.7223 6.66677 20.0001 6.66677C16.2779 6.66677 13.1251 7.95844 10.5418 10.5418C7.95844 13.1251 6.66677 16.2779 6.66677 20.0001C6.66677 23.7223 7.95844 26.8751 10.5418 29.4584C13.1251 32.0418 16.2779 33.3334 20.0001 33.3334Z" fill="#DB4437"/></g>';
break;
case 'info':
$icon = '<mask id="mask0_105_43" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><rect width="40" height="40" fill="#D9D9D9"/></mask><g mask="url(#mask0_105_43)"><path d="M18.3333 28.3333H21.6666V18.3333H18.3333V28.3333ZM20 15C20.4722 15 20.8683 14.84 21.1883 14.52C21.5072 14.2011 21.6666 13.8056 21.6666 13.3333C21.6666 12.8611 21.5072 12.465 21.1883 12.145C20.8683 11.8261 20.4722 11.6667 20 11.6667C19.5278 11.6667 19.1322 11.8261 18.8133 12.145C18.4933 12.465 18.3333 12.8611 18.3333 13.3333C18.3333 13.8056 18.4933 14.2011 18.8133 14.52C19.1322 14.84 19.5278 15 20 15ZM20 36.6667C17.6944 36.6667 15.5278 36.2289 13.5 35.3533C11.4722 34.4789 9.70831 33.2917 8.20831 31.7917C6.70831 30.2917 5.52109 28.5278 4.64665 26.5C3.77109 24.4722 3.33331 22.3056 3.33331 20C3.33331 17.6944 3.77109 15.5278 4.64665 13.5C5.52109 11.4722 6.70831 9.70833 8.20831 8.20833C9.70831 6.70833 11.4722 5.52056 13.5 4.645C15.5278 3.77056 17.6944 3.33333 20 3.33333C22.3055 3.33333 24.4722 3.77056 26.5 4.645C28.5278 5.52056 30.2916 6.70833 31.7916 8.20833C33.2916 9.70833 34.4789 11.4722 35.3533 13.5C36.2289 15.5278 36.6666 17.6944 36.6666 20C36.6666 22.3056 36.2289 24.4722 35.3533 26.5C34.4789 28.5278 33.2916 30.2917 31.7916 31.7917C30.2916 33.2917 28.5278 34.4789 26.5 35.3533C24.4722 36.2289 22.3055 36.6667 20 36.6667ZM20 33.3333C23.7222 33.3333 26.875 32.0417 29.4583 29.4583C32.0416 26.875 33.3333 23.7222 33.3333 20C33.3333 16.2778 32.0416 13.125 29.4583 10.5417C26.875 7.95833 23.7222 6.66667 20 6.66667C16.2778 6.66667 13.125 7.95833 10.5416 10.5417C7.95831 13.125 6.66665 16.2778 6.66665 20C6.66665 23.7222 7.95831 26.875 10.5416 29.4583C13.125 32.0417 16.2778 33.3333 20 33.3333Z" fill="#4285F4"/></g>';
break;
case 'success':
$icon = '<mask id="mask0_105_31" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><rect width="40" height="40" fill="#D9D9D9"/></mask><g mask="url(#mask0_105_31)"><path d="M17.6666 27.6667L29.4166 15.9167L27.0833 13.5833L17.6666 23L12.9166 18.25L10.5833 20.5833L17.6666 27.6667ZM20 36.6667C17.6944 36.6667 15.5278 36.2289 13.5 35.3533C11.4722 34.4789 9.70831 33.2917 8.20831 31.7917C6.70831 30.2917 5.52109 28.5278 4.64665 26.5C3.77109 24.4722 3.33331 22.3056 3.33331 20C3.33331 17.6945 3.77109 15.5278 4.64665 13.5C5.52109 11.4722 6.70831 9.70834 8.20831 8.20834C9.70831 6.70834 11.4722 5.52057 13.5 4.64501C15.5278 3.77057 17.6944 3.33334 20 3.33334C22.3055 3.33334 24.4722 3.77057 26.5 4.64501C28.5278 5.52057 30.2916 6.70834 31.7916 8.20834C33.2916 9.70834 34.4789 11.4722 35.3533 13.5C36.2289 15.5278 36.6666 17.6945 36.6666 20C36.6666 22.3056 36.2289 24.4722 35.3533 26.5C34.4789 28.5278 33.2916 30.2917 31.7916 31.7917C30.2916 33.2917 28.5278 34.4789 26.5 35.3533C24.4722 36.2289 22.3055 36.6667 20 36.6667ZM20 33.3333C23.7222 33.3333 26.875 32.0417 29.4583 29.4583C32.0416 26.875 33.3333 23.7222 33.3333 20C33.3333 16.2778 32.0416 13.125 29.4583 10.5417C26.875 7.95834 23.7222 6.66668 20 6.66668C16.2778 6.66668 13.125 7.95834 10.5416 10.5417C7.95831 13.125 6.66665 16.2778 6.66665 20C6.66665 23.7222 7.95831 26.875 10.5416 29.4583C13.125 32.0417 16.2778 33.3333 20 33.3333Z" fill="#0F9D58"/></g>';
break;
}
$this->payload['icon'] = $icon;
}
// Set whether dismissible
if ($this->payload['dismissible'])
{
$this->payload['class'] .= ' alert-dismissible';
}
}
/**
* Whether the notice can run.
*
* @return bool
*/
protected function canRun()
{
// If no title or description is given, do not run
if (empty($this->payload['title']) && empty($this->payload['description']))
{
return false;
}
return true;
}
/**
* Returns the date difference between today and a given date in the future.
*
* @param string $date1
* @param string $date2
*
* @return string
*/
protected function getDaysDifference($date1, $date2)
{
return (int) round(($date1 - $date2) / (60 * 60 * 24));
}
} Notices/Notices/Expired.php 0000644 00000004203 15235314577 0011700 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Functions;
use \NRFramework\Extension;
class Expired extends Notice
{
protected $notice_payload = [
'type' => 'error',
'class' => 'expired',
'expired_at' => '',
'plan' => ''
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['tooltip'] = Text::_('NR_NOTICE_EXPIRED_TOOLTIP');
$this->payload['expired_at'] = isset($payload['expired_at']) ? $payload['expired_at'] : false;
$this->payload['plan'] = isset($payload['plan']) ? $payload['plan'] : false;
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_SUBSCRIPTION_EXPIRED'), $this->extension_name);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
$title = strtolower($this->payload['plan']) === 'bundle' ? $this->payload['plan'] : $this->extension_name . ' ' . $this->payload['plan'];
return sprintf(Text::_('NR_SUBSCRIPTION_EXPIRED_DESC'), $title, Functions::applySiteTimezoneToDate($this->payload['expired_at'], 'd M o'));
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = 'https://www.tassos.gr/subscriptions';
return '<a href="' . Functions::getUTMURL($url, 'UserNotice', 'SubscriptionExpired') . '" target="_blank" class="tf-notice-btn info">' . sprintf(Text::_('NR_RENEW_X_PERCENT_OFF'), 20) . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, it's already hidden
if ($this->factory->getCookie('tfNoticeHideExpiredNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
// The date the extension expired.
if (!$this->payload['expired_at'])
{
return false;
}
return true;
}
} Notices/Notices/Geolocation.php 0000644 00000003353 15235314577 0012550 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use \NRFramework\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Session\Session;
class Geolocation extends Notice
{
protected $notice_payload = [
'type' => 'warning',
'class' => 'geolocation'
];
public function __construct($payload = [])
{
parent::__construct($payload);
\NRFramework\Functions::loadLanguage('plg_system_tgeoip');
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return Text::_('PLG_SYSTEM_TGEOIP_MAINTENANCE');
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return sprintf(Text::_('NR_NOTICE_GEO_MAINTENANCE_DESC'), $this->extension_name);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = Uri::base() . 'index.php?option=com_ajax&format=raw&plugin=tgeoip&task=update-red&' . Session::getFormToken() . '=1&return=' . base64_encode($this->payload['current_url']);
return '<a href="' . $url . '" class="tf-notice-btn info">' . Text::_('NR_UPDATE_NOW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, its been hidden
if ($this->factory->getCookie('tfNoticeHideGeolocationNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
if (!Extension::geoPluginNeedsUpdate())
{
return false;
}
return true;
}
} Notices/Notices/UpgradeToPro.php 0000644 00000004411 15235314577 0012654 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Extension;
class UpgradeToPro extends Notice
{
/**
* Define how old (in days) the extension needs to be since the installation date
* in order to display this notice.
*
* @var int
*/
private $upgrade_to_pro_notice_days_old = 30;
protected $notice_payload = [
'type' => 'success',
'class' => 'upgradeToPro'
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['tooltip'] = Text::_('NR_NOTICE_UPGRADE_TO_PRO_TOOLTIP');
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_UPGRADE_TO_PRO_X_OFF'), 20);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return sprintf(Text::_('NR_UPGRADE_TO_PRO_NOTICE_DESC'), $this->extension_name);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = Extension::getTassosExtensionUpgradeURL($this->payload['ext_xml'], false);
return '<a href="' . \NRFramework\Functions::getUTMURL($url, 'UserNotice', 'UpgradeToPro') . '" target="_blank" class="tf-notice-btn success">' . Text::_('NR_UPGRADE_NOW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, its been hidden
if ($this->factory->getCookie('tfNoticeHideUpgradeToProNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
// If its already Pro, abort
if (Extension::isPro($this->payload['ext_xml']))
{
return false;
}
// Get extension installation date
if (!$install_date = Extension::getInstallationDate($this->payload['ext_element']))
{
return false;
}
// If the extension is not old enough, do not show the rate notice
if ($this->getDaysDifference(time(), strtotime($install_date)) < $this->upgrade_to_pro_notice_days_old)
{
return false;
}
return true;
}
} Notices/Notices/UpgradeToBundle.php 0000644 00000005417 15235314577 0013334 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Extension;
class UpgradeToBundle extends Notice
{
/**
* Define how old (in days) the extension needs to be since the installation date
* in order to display this notice.
*
* @var int
*/
private $upgrade_to_bundle_notice_days_old = 60;
protected $notice_payload = [
'type' => 'success',
'class' => 'upgradeToBundle'
];
public function __construct($payload = [])
{
parent::__construct($payload);
$this->payload['tooltip'] = Text::_('NR_NOTICE_UPGRADE_TO_BUNDLE_TOOLTIP');
}
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return Text::_('NR_UPGRADE_TO_BUNDLE');
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return Text::_('NR_UPGRADE_TO_BUNDLE_NOTICE_DESC');
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
$url = 'https://www.tassos.gr/subscriptions';
return '<a href="' . \NRFramework\Functions::getUTMURL($url, 'UserNotice', 'UpgradeToBundle') . '" target="_blank" class="tf-notice-btn success">' . Text::_('NR_UPGRADE_NOW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, its been hidden
if ($this->factory->getCookie('tfNoticeHideUpgradeToBundleNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
// Get license details for this extension
if ($details = \NRFramework\Notices\Helper::getExtensionDetails($this->payload['license_data'], $this->payload['ext_element']))
{
// If we already have an active bundle plan, abort
if (isset($details['active']) && isset($details['plan']) && $details['active'] && strtolower($details['plan']) === 'bundle')
{
return false;
}
}
// The user must have at least 2 installed tassos.gr extensions
if (Extension::getTotalInstalledExtensions() < 2)
{
return false;
}
// User must have at least 1 paid subscription
if (Extension::getUserTotalPaidPlans($this->payload['license_data']) < 1)
{
return false;
}
// Get extension installation date
if (!$install_date = Extension::getInstallationDate($this->payload['ext_element']))
{
return false;
}
// If the extension is not old enough, do not show the rate notice
if ($this->getDaysDifference(time(), strtotime($install_date)) < $this->upgrade_to_bundle_notice_days_old)
{
return false;
}
return true;
}
} Notices/Notices/Outdated.php 0000644 00000003374 15235314577 0012061 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Functions;
use \NRFramework\Extension;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Session\Session;
class Outdated extends Notice
{
/**
* How old the extension needs to be to be defined as "outdated".
*
* @var int
*/
private $oudated_notice_days_old = 120;
protected $notice_payload = [
'type' => 'warning',
'class' => 'outdated'
];
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_EXTENSION_IS_OUTDATED'), $this->extension_name);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return sprintf(Text::_('NR_OUTDATED_EXTENSION'), $this->extension_name, $this->oudated_notice_days_old);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
return '<a href="' . Uri::base() . 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1" class="tf-notice-btn info">' . Text::_('NR_UPDATE_NOW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, its been hidden
if ($this->factory->getCookie('tfNoticeHideOutdatedNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
if (!Extension::isOutdated($this->payload['ext_element'], $this->oudated_notice_days_old))
{
return false;
}
return true;
}
} Notices/Notices/Rate.php 0000644 00000003720 15235314577 0011176 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Notices\Notices;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use \NRFramework\Extension;
class Rate extends Notice
{
/**
* Define how old (in days) the extension needs to be since the installation date
* in order to display this notice.
*
* @var int
*/
private $rate_notice_days_old = 10;
protected $notice_payload = [
'type' => 'info',
'class' => 'rate'
];
/**
* Notice title.
*
* @return string
*/
protected function getTitle()
{
return sprintf(Text::_('NR_RATE'), $this->extension_name);
}
/**
* Notice description.
*
* @return string
*/
protected function getDescription()
{
return sprintf(Text::_('NR_RATE_NOTICE_EXTENSION_DESC'), $this->extension_name);
}
/**
* Notice actions.
*
* @return string
*/
protected function getActions()
{
return '<a href="#" class="tf-rate-already-rated">' . Text::_('NR_I_ALREADY_DID') . '</a>
<a href="' . Extension::getExtensionJEDURL($this->payload['ext_xml']) . '#reviews" target="_blank" class="tf-notice-btn info">' . Text::_('NR_WRITE_A_REVIEW') . '</a>';
}
/**
* Whether the notice can run.
*
* @return string
*/
protected function canRun()
{
// If cookie exists, it's already hidden
if ($this->factory->getCookie('tfNoticeHideRateNotice_' . $this->payload['ext_element']) === 'true')
{
return false;
}
// Get extension installation date
if (!$install_date = Extension::getInstallationDate($this->payload['ext_element']))
{
return false;
}
// If the extension is not old enough, do not show the rate notice
if ($this->getDaysDifference(time(), strtotime($install_date)) < $this->rate_notice_days_old)
{
return false;
}
return true;
}
} Vendor/MobileDetect.php 0000644 00000232547 15235314577 0011103 0 ustar 00 <?php
/**
* Mobile Detect Library
* Motto: "Every business should have a mobile detection script to detect mobile readers"
*
* Mobile_Detect is a lightweight PHP class for detecting mobile devices (including tablets).
* It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.
*
* Homepage: http://mobiledetect.net
* GitHub: https://github.com/serbanghita/Mobile-Detect
* README: https://github.com/serbanghita/Mobile-Detect/blob/master/README.md
* CONTRIBUTING: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/CONTRIBUTING.md
* KNOWN LIMITATIONS: https://github.com/serbanghita/Mobile-Detect/blob/master/docs/KNOWN_LIMITATIONS.md
* EXAMPLES: https://github.com/serbanghita/Mobile-Detect/wiki/Code-examples
*
* @license https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
* @author Serban Ghita <serbanghita@gmail.com> (since 2012)
* @author Nick Ilyin <nick.ilyin@gmail.com>
* @author: Victor Stanciu <vic.stanciu@gmail.com> (original author)
*
* @version 3.74.0
*
* Auto-generated isXXXX() magic methods.
* php -a export/dump_magic_methods.php
*
* @method bool isiPhone()
* @method bool isBlackBerry()
* @method bool isPixel()
* @method bool isHTC()
* @method bool isNexus()
* @method bool isDell()
* @method bool isMotorola()
* @method bool isSamsung()
* @method bool isLG()
* @method bool isSony()
* @method bool isAsus()
* @method bool isXiaomi()
* @method bool isNokiaLumia()
* @method bool isMicromax()
* @method bool isPalm()
* @method bool isVertu()
* @method bool isPantech()
* @method bool isFly()
* @method bool isWiko()
* @method bool isiMobile()
* @method bool isSimValley()
* @method bool isWolfgang()
* @method bool isAlcatel()
* @method bool isNintendo()
* @method bool isAmoi()
* @method bool isINQ()
* @method bool isOnePlus()
* @method bool isGenericPhone()
* @method bool isiPad()
* @method bool isNexusTablet()
* @method bool isGoogleTablet()
* @method bool isSamsungTablet()
* @method bool isKindle()
* @method bool isSurfaceTablet()
* @method bool isHPTablet()
* @method bool isAsusTablet()
* @method bool isBlackBerryTablet()
* @method bool isHTCtablet()
* @method bool isMotorolaTablet()
* @method bool isNookTablet()
* @method bool isAcerTablet()
* @method bool isToshibaTablet()
* @method bool isLGTablet()
* @method bool isFujitsuTablet()
* @method bool isPrestigioTablet()
* @method bool isLenovoTablet()
* @method bool isDellTablet()
* @method bool isYarvikTablet()
* @method bool isMedionTablet()
* @method bool isArnovaTablet()
* @method bool isIntensoTablet()
* @method bool isIRUTablet()
* @method bool isMegafonTablet()
* @method bool isEbodaTablet()
* @method bool isAllViewTablet()
* @method bool isArchosTablet()
* @method bool isAinolTablet()
* @method bool isNokiaLumiaTablet()
* @method bool isSonyTablet()
* @method bool isPhilipsTablet()
* @method bool isCubeTablet()
* @method bool isCobyTablet()
* @method bool isMIDTablet()
* @method bool isMSITablet()
* @method bool isSMiTTablet()
* @method bool isRockChipTablet()
* @method bool isFlyTablet()
* @method bool isbqTablet()
* @method bool isHuaweiTablet()
* @method bool isNecTablet()
* @method bool isPantechTablet()
* @method bool isBronchoTablet()
* @method bool isVersusTablet()
* @method bool isZyncTablet()
* @method bool isPositivoTablet()
* @method bool isNabiTablet()
* @method bool isKoboTablet()
* @method bool isDanewTablet()
* @method bool isTexetTablet()
* @method bool isPlaystationTablet()
* @method bool isTrekstorTablet()
* @method bool isPyleAudioTablet()
* @method bool isAdvanTablet()
* @method bool isDanyTechTablet()
* @method bool isGalapadTablet()
* @method bool isMicromaxTablet()
* @method bool isKarbonnTablet()
* @method bool isAllFineTablet()
* @method bool isPROSCANTablet()
* @method bool isYONESTablet()
* @method bool isChangJiaTablet()
* @method bool isGUTablet()
* @method bool isPointOfViewTablet()
* @method bool isOvermaxTablet()
* @method bool isHCLTablet()
* @method bool isDPSTablet()
* @method bool isVistureTablet()
* @method bool isCrestaTablet()
* @method bool isMediatekTablet()
* @method bool isConcordeTablet()
* @method bool isGoCleverTablet()
* @method bool isModecomTablet()
* @method bool isVoninoTablet()
* @method bool isECSTablet()
* @method bool isStorexTablet()
* @method bool isVodafoneTablet()
* @method bool isEssentielBTablet()
* @method bool isRossMoorTablet()
* @method bool isiMobileTablet()
* @method bool isTolinoTablet()
* @method bool isAudioSonicTablet()
* @method bool isAMPETablet()
* @method bool isSkkTablet()
* @method bool isTecnoTablet()
* @method bool isJXDTablet()
* @method bool isiJoyTablet()
* @method bool isFX2Tablet()
* @method bool isXoroTablet()
* @method bool isViewsonicTablet()
* @method bool isVerizonTablet()
* @method bool isOdysTablet()
* @method bool isCaptivaTablet()
* @method bool isIconbitTablet()
* @method bool isTeclastTablet()
* @method bool isOndaTablet()
* @method bool isJaytechTablet()
* @method bool isBlaupunktTablet()
* @method bool isDigmaTablet()
* @method bool isEvolioTablet()
* @method bool isLavaTablet()
* @method bool isAocTablet()
* @method bool isMpmanTablet()
* @method bool isCelkonTablet()
* @method bool isWolderTablet()
* @method bool isMediacomTablet()
* @method bool isMiTablet()
* @method bool isNibiruTablet()
* @method bool isNexoTablet()
* @method bool isLeaderTablet()
* @method bool isUbislateTablet()
* @method bool isPocketBookTablet()
* @method bool isKocasoTablet()
* @method bool isHisenseTablet()
* @method bool isHudl()
* @method bool isTelstraTablet()
* @method bool isGenericTablet()
* @method bool isAndroidOS()
* @method bool isBlackBerryOS()
* @method bool isPalmOS()
* @method bool isSymbianOS()
* @method bool isWindowsMobileOS()
* @method bool isWindowsPhoneOS()
* @method bool isiOS()
* @method bool isiPadOS()
* @method bool isSailfishOS()
* @method bool isMeeGoOS()
* @method bool isMaemoOS()
* @method bool isJavaOS()
* @method bool iswebOS()
* @method bool isbadaOS()
* @method bool isBREWOS()
* @method bool isChrome()
* @method bool isDolfin()
* @method bool isOpera()
* @method bool isSkyfire()
* @method bool isEdge()
* @method bool isIE()
* @method bool isFirefox()
* @method bool isBolt()
* @method bool isTeaShark()
* @method bool isBlazer()
* @method bool isSafari()
* @method bool isWeChat()
* @method bool isUCBrowser()
* @method bool isbaiduboxapp()
* @method bool isbaidubrowser()
* @method bool isDiigoBrowser()
* @method bool isMercury()
* @method bool isObigoBrowser()
* @method bool isNetFront()
* @method bool isGenericBrowser()
* @method bool isPaleMoon()
* @method bool isBot()
* @method bool isMobileBot()
* @method bool isDesktopMode()
* @method bool isTV()
* @method bool isWebKit()
* @method bool isConsole()
* @method bool isWatch()
*/
namespace NRFramework\Vendor;
defined('_JEXEC') or die;
use BadMethodCallException;
class MobileDetect
{
/**
* A frequently used regular expression to extract version #s.
*
* @deprecated since version 2.6.9
*/
const VER = '([\w._\+]+)';
/**
* Stores the version number of the current release.
*/
const VERSION = '3.74.0';
/**
* A type for the version() method indicating a string return value.
*/
const VERSION_TYPE_STRING = 'text';
/**
* A type for the version() method indicating a float return value.
*/
const VERSION_TYPE_FLOAT = 'float';
/**
* A cache for resolved matches
* @var array
*/
protected array $cache = [];
/**
* The User-Agent HTTP header is stored in here.
* @var string|null
*/
protected ?string $userAgent = null;
/**
* HTTP headers in the PHP-flavor. So HTTP_USER_AGENT and SERVER_SOFTWARE.
* @var array
*/
protected array $httpHeaders = [];
/**
* CloudFront headers. E.g. CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer & CloudFront-Is-Tablet-Viewer.
* @var array
*/
protected array $cloudfrontHeaders = [];
/**
* The matching Regex.
* This is good for debug.
* @var string|null
*/
protected ?string $matchingRegex = null;
/**
* The matches extracted from the regex expression.
* This is good for debug.
*
* @var array|null
*/
protected ?array $matchesArray = null;
/**
* HTTP headers that trigger the 'isMobile' detection
* to be true.
*
* @var array
*/
protected static array $mobileHeaders = [
'HTTP_ACCEPT' => [
'matches' => [
// Opera Mini
// @reference: http://dev.opera.com/articles/view/opera-binary-markup-language/
'application/x-obml2d',
// BlackBerry devices.
'application/vnd.rim.html',
'text/vnd.wap.wml',
'application/vnd.wap.xhtml+xml'
]],
'HTTP_X_WAP_PROFILE' => null,
'HTTP_X_WAP_CLIENTID' => null,
'HTTP_WAP_CONNECTION' => null,
'HTTP_PROFILE' => null,
// Reported by Opera on Nokia devices (eg. C3).
'HTTP_X_OPERAMINI_PHONE_UA' => null,
'HTTP_X_NOKIA_GATEWAY_ID' => null,
'HTTP_X_ORANGE_ID' => null,
'HTTP_X_VODAFONE_3GPDPCONTEXT' => null,
'HTTP_X_HUAWEI_USERID' => null,
// Reported by Windows Smartphones.
'HTTP_UA_OS' => null,
// Reported by Verizon, Vodafone proxy system.
'HTTP_X_MOBILE_GATEWAY' => null,
// Seen this on HTC Sensation. SensationXE_Beats_Z715e.
'HTTP_X_ATT_DEVICEID' => null,
// Seen this on a HTC.
'HTTP_UA_CPU' => ['matches' => ['ARM']],
];
/**
* List of mobile devices (phones).
*
* @var array
*/
protected static array $phoneDevices = [
'iPhone' => '\biPhone\b|\biPod\b', // |\biTunes
'BlackBerry' => 'BlackBerry|\bBB10\b|rim[0-9]+|\b(BBA100|BBB100|BBD100|BBE100|BBF100|STH100)\b-[0-9]+',
'Pixel' => '; \bPixel\b',
'HTC' => 'HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6400L|ADR6425|001HT|Inspire 4G|Android.*\bEVO\b|T-Mobile G1|Z520m|Android [0-9.]+; Pixel',
'Nexus' => 'Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile|Nexus 4|Nexus 5|Nexus 5X|Nexus 6',
// @todo: Is 'Dell Streak' a tablet or a phone? ;)
'Dell' => 'Dell[;]? (Streak|Aero|Venue|Venue Pro|Flash|Smoke|Mini 3iX)|XCD28|XCD35|\b001DL\b|\b101DL\b|\bGS01\b',
'Motorola' => 'Motorola|DROIDX|DROID BIONIC|\bDroid\b.*Build|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT901|XT907|XT909|XT910|XT912|XT928|XT926|XT915|XT919|XT925|XT1021|\bMoto E\b|XT1068|XT1092|XT1052',
'Samsung' => '\bSamsung\b|SM-G950F|SM-G955F|SM-G9250|GT-19300|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8190|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9082|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9305|GT-I9500|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S7710|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-I959|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|GT-N7105|SCH-I535|SM-N900A|SGH-I317|SGH-T999L|GT-S5360B|GT-I8262|GT-S6802|GT-S6312|GT-S6310|GT-S5312|GT-S5310|GT-I9105|GT-I8510|GT-S6790N|SM-G7105|SM-N9005|GT-S5301|GT-I9295|GT-I9195|SM-C101|GT-S7392|GT-S7560|GT-B7610|GT-I5510|GT-S7582|GT-S7530E|GT-I8750|SM-G9006V|SM-G9008V|SM-G9009D|SM-G900A|SM-G900D|SM-G900F|SM-G900H|SM-G900I|SM-G900J|SM-G900K|SM-G900L|SM-G900M|SM-G900P|SM-G900R4|SM-G900S|SM-G900T|SM-G900V|SM-G900W8|SHV-E160K|SCH-P709|SCH-P729|SM-T2558|GT-I9205|SM-G9350|SM-J120F|SM-G920F|SM-G920V|SM-G930F|SM-N910C|SM-A310F|GT-I9190|SM-J500FN|SM-G903F|SM-J330F|SM-G610F|SM-G981B|SM-G892A|SM-A530F|SM-G988N|SM-G781B|SM-A805N|SM-G965F',
'LG' => '\bLG\b;|LG[- ]?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS740|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P500|P509|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999|E612|D955|D802|MS323|M257)|LM-G710',
'Sony' => 'SonyST|SonyLT|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i|LT28h|LT26w|SonyEricssonMT27i|C5303|C6902|C6903|C6906|C6943|D2533|SOV34|601SO|F8332',
'Asus' => 'Asus.*Galaxy|PadFone.*Mobile|ASUS_Z01QD|ASUS_X00TD',
'Xiaomi' => '^(?!.*\bx11\b).*xiaomi.*$|POCOPHONE F1|\bMI\b 8|\bMi\b 10|Redmi Note 9S|Redmi 5A|Redmi Note 5A Prime|Redmi Note 7 Pro|N2G47H|M2001J2G|M2001J2I|M1805E10A|M2004J11G|M1902F1G|M2002J9G|M2004J19G|M2003J6A1G|M2012K11C|M2007J1SC',
'NokiaLumia' => 'Lumia [0-9]{3,4}',
// http://www.micromaxinfo.com/mobiles/smartphones
// Added because the codes might conflict with Acer Tablets.
'Micromax' => 'Micromax.*\b(A210|A92|A88|A72|A111|A110Q|A115|A116|A110|A90S|A26|A51|A35|A54|A25|A27|A89|A68|A65|A57|A90)\b',
// @todo Complete the regex.
'Palm' => 'PalmSource|Palm', // avantgo|blazer|elaine|hiptop|plucker|xiino ;
'Vertu' => 'Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature', // Just for fun ;)
// http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA (PANTECH)
// Most of the VEGA devices are legacy. PANTECH seem to be newer devices based on Android.
'Pantech' => 'PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790',
// http://www.fly-phone.com/devices/smartphones/ ; Included only smartphones.
'Fly' => 'IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250',
// http://fr.wikomobile.com
'Wiko' => 'KITE 4G|HIGHWAY|GETAWAY|STAIRWAY|DARKSIDE|DARKFULL|DARKNIGHT|DARKMOON|SLIDE|WAX 4G|RAINBOW|BLOOM|SUNSET|GOA(?!nna)|LENNY|BARRY|IGGY|OZZY|CINK FIVE|CINK PEAX|CINK PEAX 2|CINK SLIM|CINK SLIM 2|CINK +|CINK KING|CINK PEAX|CINK SLIM|SUBLIM',
'iMobile' => 'i-mobile (IQ|i-STYLE|idea|ZAA|Hitz)',
// Added simvalley mobile just for fun. They have some interesting devices.
// http://www.simvalley.fr/telephonie---gps-_22_telephonie-mobile_telephones_.html
'SimValley' => '\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\b',
// Wolfgang - a brand that is sold by Aldi supermarkets.
// http://www.wolfgangmobile.com/
'Wolfgang' => 'AT-B24D|AT-AS50HD|AT-AS40W|AT-AS55HD|AT-AS45q2|AT-B26D|AT-AS50Q',
'Alcatel' => 'Alcatel',
'Nintendo' => 'Nintendo (3DS|Switch)',
// http://en.wikipedia.org/wiki/Amoi
'Amoi' => 'Amoi',
// http://en.wikipedia.org/wiki/INQ
'INQ' => 'INQ',
'OnePlus' => 'ONEPLUS',
// @Tapatalk is a mobile app; http://support.tapatalk.com/threads/smf-2-0-2-os-and-browser-detection-plugin-and-tapatalk.15565/#post-79039
'GenericPhone' => 'Tapatalk|PDA;|SAGEM|\bmmp\b|pocket|\bpsp\b|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|\bwap\b|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser',
];
/**
* List of tablet devices.
*
* @var array
*/
protected static array $tabletDevices = [
// @todo: check for mobile friendly emails topic.
'iPad' => 'iPad|iPad.*Mobile',
// Removed |^.*Android.*Nexus(?!(?:Mobile).)*$
// @see #442
// @todo Merge NexusTablet into GoogleTablet.
'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)',
// https://en.wikipedia.org/wiki/Pixel_C
'GoogleTablet' => 'Android.*Pixel C',
'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone.
// http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)',
// Only the Surface tablets with Windows RT are considered mobile.
// http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx
'SurfaceTablet' => 'Windows NT [0-9.]+; ARM;.*(Tablet|ARMBJS)',
// http://shopping1.hp.com/is-bin/INTERSHOP.enfinity/WFS/WW-USSMBPublicStore-Site/en_US/-/USD/ViewStandardCatalog-Browse?CatalogCategoryID=JfIQ7EN5lqMAAAEyDcJUDwMT
'HPTablet' => 'HP Slate (7|8|10)|HP ElitePad 900|hp-tablet|EliteBook.*Touch|HP 8|Slate 21|HP SlateBook 10',
// Watch out for PadFone, see #132.
// http://www.asus.com/de/Tablets_Mobile/Memo_Pad_Products/
'AsusTablet' => '^.*PadFone((?!Mobile).)*$|Transformer|TF101|TF101G|TF300T|TF300TG|TF300TL|TF700T|TF700KL|TF701T|TF810C|ME171|ME301T|ME302C|ME371MG|ME370T|ME372MG|ME172V|ME173X|ME400C|Slider SL101|\bK00F\b|\bK00C\b|\bK00E\b|\bK00L\b|TX201LA|ME176C|ME102A|\bM80TA\b|ME372CL|ME560CG|ME372CG|ME302KL| K01A | K010 | K011 | K017 | K01E |ME572C|ME103K|ME170C|ME171C|\bME70C\b|ME581C|ME581CL|ME8510C|ME181C|P01Y|PO1MA|P01Z|\bP027\b|\bP024\b|\bP00C\b',
'BlackBerryTablet' => 'PlayBook|RIM Tablet',
'HTCtablet' => 'HTC_Flyer_P512|HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200|PG09410',
'MotorolaTablet' => 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617',
'NookTablet' => 'Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2',
// http://www.acer.ro/ac/ro/RO/content/drivers
// http://www.packardbell.co.uk/pb/en/GB/content/download (Packard Bell is part of Acer)
// http://us.acer.com/ac/en/US/content/group/tablets
// http://www.acer.de/ac/de/DE/content/models/tablets/
// Can conflict with Micromax and Motorola phones codes.
'AcerTablet' => 'Android.*; \b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71|B1-710|B1-711|A1-810|A1-811|A1-830)\b|W3-810|\bA3-A10\b|\bA3-A11\b|\bA3-A20\b|\bA3-A30|A3-A40',
// http://eu.computers.toshiba-europe.com/innovation/family/Tablets/1098744/banner_id/tablet_footerlink/
// http://us.toshiba.com/tablets/tablet-finder
// http://www.toshiba.co.jp/regza/tablet/
'ToshibaTablet' => 'Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO',
// http://www.nttdocomo.co.jp/english/service/developer/smart_phone/technical_info/spec/index.html
// http://www.lg.com/us/tablets
'LGTablet' => '\bL-06C|LG-V909|LG-V900|LG-V700|LG-V510|LG-V500|LG-V410|LG-V400|LG-VK810\b',
'FujitsuTablet' => 'Android.*\b(F-01D|F-02F|F-05E|F-10D|M532|Q572)\b',
// Prestigio Tablets http://www.prestigio.com/support
'PrestigioTablet' => 'PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280C3G|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474|PMP5097CPRO|PMP5097|PMP7380D|PMP5297C|PMP5297C_QUAD|PMP812E|PMP812E3G|PMP812F|PMP810E|PMP880TD|PMT3017|PMT3037|PMT3047|PMT3057|PMT7008|PMT5887|PMT5001|PMT5002',
// http://support.lenovo.com/en_GB/downloads/default.page?#
'LenovoTablet' => 'Lenovo TAB|Idea(Tab|Pad)( A1|A10| K1|)|ThinkPad([ ]+)?Tablet|YT3-850M|YT3-X90L|YT3-X90F|YT3-X90X|Lenovo.*(S2109|S2110|S5000|S6000|K3011|A3000|A3500|A1000|A2107|A2109|A1107|A5500|A7600|B6000|B8000|B8080)(-|)(FL|F|HV|H|)|TB-X103F|TB-X304X|TB-X304F|TB-X304L|TB-X505F|TB-X505L|TB-X505X|TB-X605F|TB-X605L|TB-8703F|TB-8703X|TB-8703N|TB-8704N|TB-8704F|TB-8704X|TB-8704V|TB-7304F|TB-7304I|TB-7304X|Tab2A7-10F|Tab2A7-20F|TB2-X30L|YT3-X50L|YT3-X50F|YT3-X50M|YT-X705F|YT-X703F|YT-X703L|YT-X705L|YT-X705X|TB2-X30F|TB2-X30L|TB2-X30M|A2107A-F|A2107A-H|TB3-730F|TB3-730M|TB3-730X|TB-7504F|TB-7504X|TB-X704F|TB-X104F|TB3-X70F|TB-X705F|TB-8504F|TB3-X70L|TB3-710F|TB-X704L|TB-J606F|TB-X606F|TB-X306X|YT-J706X',
// http://www.dell.com/support/home/us/en/04/Products/tab_mob/tablets
'DellTablet' => 'Venue 11|Venue 8|Venue 7|Dell Streak 10|Dell Streak 7',
'XiaomiTablet' => '21051182G',
// http://www.yarvik.com/en/matrix/tablets/
'YarvikTablet' => 'Android.*\b(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468|TAB07-100|TAB07-101|TAB07-150|TAB07-151|TAB07-152|TAB07-200|TAB07-201-3G|TAB07-210|TAB07-211|TAB07-212|TAB07-214|TAB07-220|TAB07-400|TAB07-485|TAB08-150|TAB08-200|TAB08-201-3G|TAB08-201-30|TAB09-100|TAB09-211|TAB09-410|TAB10-150|TAB10-201|TAB10-211|TAB10-400|TAB10-410|TAB13-201|TAB274EUK|TAB275EUK|TAB374EUK|TAB462EUK|TAB474EUK|TAB9-200)\b',
'MedionTablet' => 'Android.*\bOYO\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB',
'ArnovaTablet' => '97G4|AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT|AN9G2',
// http://www.intenso.de/kategorie_en.php?kategorie=33
// @todo: http://www.nbhkdz.com/read/b8e64202f92a2df129126bff.html - investigate
'IntensoTablet' => 'INM8002KP|INM1010FP|INM805ND|Intenso Tab|TAB1004',
// IRU.ru Tablets http://www.iru.ru/catalog/soho/planetable/
'IRUTablet' => 'M702pro',
'MegafonTablet' => 'MegaFon V9|\bZTE V9\b|Android.*\bMT7A\b',
// http://www.e-boda.ro/tablete-pc.html
'EbodaTablet' => 'E-Boda (Supreme|Impresspeed|Izzycomm|Essential)',
// http://www.allview.ro/produse/droseries/lista-tablete-pc/
'AllViewTablet' => 'Allview.*(Viva|Alldro|City|Speed|All TV|Frenzy|Quasar|Shine|TX1|AX1|AX2)',
// http://wiki.archosfans.com/index.php?title=Main_Page
// @note Rewrite the regex format after we add more UAs.
'ArchosTablet' => '\b(101G9|80G9|A101IT)\b|Qilive 97R|Archos5|\bARCHOS (70|79|80|90|97|101|FAMILYPAD|)(b|c|)(G10| Cobalt| TITANIUM(HD|)| Xenon| Neon|XSK| 2| XS 2| PLATINUM| CARBON|GAMEPAD)\b',
// http://www.ainol.com/plugin.php?identifier=ainol&module=product
'AinolTablet' => 'NOVO7|NOVO8|NOVO10|Novo7Aurora|Novo7Basic|NOVO7PALADIN|novo9-Spark',
'NokiaLumiaTablet' => 'Lumia 2520',
// @todo: inspect http://esupport.sony.com/US/p/select-system.pl?DIRECTOR=DRIVER
// Readers http://www.atsuhiro-me.net/ebook/sony-reader/sony-reader-web-browser
// http://www.sony.jp/support/tablet/
'SonyTablet' => 'Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT13|SGPT114|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT131|SGPT132|SGPT133|SGPT211|SGPT212|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201|SGP351|SGP341|SGP511|SGP512|SGP521|SGP541|SGP551|SGP621|SGP641|SGP612|SOT31|SGP771|SGP611|SGP612|SGP712',
// http://www.support.philips.com/support/catalog/worldproducts.jsp?userLanguage=en&userCountry=cn&categoryid=3G_LTE_TABLET_SU_CN_CARE&title=3G%20tablets%20/%20LTE%20range&_dyncharset=UTF-8
'PhilipsTablet' => '\b(PI2010|PI3000|PI3100|PI3105|PI3110|PI3205|PI3210|PI3900|PI4010|PI7000|PI7100)\b',
// db + http://www.cube-tablet.com/buy-products.html
'CubeTablet' => 'Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT',
// http://www.cobyusa.com/?p=pcat&pcat_id=3001
'CobyTablet' => 'MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7015|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010',
// http://www.match.net.cn/products.asp
'MIDTablet' => 'M9701|M9000|M9100|M806|M1052|M806|T703|MID701|MID713|MID710|MID727|MID760|MID830|MID728|MID933|MID125|MID810|MID732|MID120|MID930|MID800|MID731|MID900|MID100|MID820|MID735|MID980|MID130|MID833|MID737|MID960|MID135|MID860|MID736|MID140|MID930|MID835|MID733|MID4X10',
// http://www.msi.com/support
// @todo Research the Windows Tablets.
'MSITablet' => 'MSI \b(Primo 73K|Primo 73L|Primo 81L|Primo 77|Primo 93|Primo 75|Primo 76|Primo 73|Primo 81|Primo 91|Primo 90|Enjoy 71|Enjoy 7|Enjoy 10)\b',
// @todo http://www.kyoceramobile.com/support/drivers/
// 'KyoceraTablet' => null,
// @todo http://intexuae.com/index.php/category/mobile-devices/tablets-products/
// 'IntextTablet' => null,
// http://pdadb.net/index.php?m=pdalist&list=SMiT (NoName Chinese Tablets)
// http://www.imp3.net/14/show.php?itemid=20454
'SMiTTablet' => 'Android.*(\bMID\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)',
// http://www.rock-chips.com/index.php?do=prod&pid=2
'RockChipTablet' => 'Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A',
// http://www.fly-phone.com/devices/tablets/ ; http://www.fly-phone.com/service/
'FlyTablet' => 'IQ310|Fly Vision',
// http://www.bqreaders.com/gb/tablets-prices-sale.html
'bqTablet' => 'Android.*(bq)?.*\b(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant|Aquaris ([E|M]10|M8))\b|Maxwell.*Lite|Maxwell.*Plus',
// http://www.huaweidevice.com/worldwide/productFamily.do?method=index&directoryId=5011&treeId=3290
// http://www.huaweidevice.com/worldwide/downloadCenter.do?method=index&directoryId=3372&treeId=0&tb=1&type=software (including legacy tablets)
'HuaweiTablet' => 'MediaPad|MediaPad 7 Youth|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim|M2-A01L|BAH-L09|BAH-W09|AGS-L09|CMR-AL19|KOB2-L09|BG2-U01|BG2-W09|BG2-U03',
// Nec or Medias Tab
'NecTablet' => '\bN-06D|\bN-08D',
// Pantech Tablets: http://www.pantechusa.com/phones/
'PantechTablet' => 'Pantech.*P4100',
// Broncho Tablets: http://www.broncho.cn/ (hard to find)
'BronchoTablet' => 'Broncho.*(N701|N708|N802|a710)',
// http://versusuk.com/support.html
'VersusTablet' => 'TOUCHPAD.*[78910]|\bTOUCHTAB\b',
// http://www.zync.in/index.php/our-products/tablet-phablets
'ZyncTablet' => 'z1000|Z99 2G|z930|z990|z909|Z919|z900', // Removed "z999" because of https://github.com/serbanghita/Mobile-Detect/issues/717
// http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/
'PositivoTablet' => 'TB07STA|TB10STA|TB07FTA|TB10FTA',
// https://www.nabitablet.com/
'NabiTablet' => 'Android.*\bNabi',
'KoboTablet' => 'Kobo Touch|\bK080\b|\bVox\b Build|\bArc\b Build',
// French Danew Tablets http://www.danew.com/produits-tablette.php
'DanewTablet' => 'DSlide.*\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\b',
// Texet Tablets and Readers http://www.texet.ru/tablet/
'TexetTablet' => 'NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE',
// Avoid detecting 'PLAYSTATION 3' as mobile.
'PlaystationTablet' => 'Playstation.*(Portable|Vita)',
// http://www.trekstor.de/surftabs.html
'TrekstorTablet' => 'ST10416-1|VT10416-1|ST70408-1|ST702xx-1|ST702xx-2|ST80208|ST97216|ST70104-2|VT10416-2|ST10216-2A|SurfTab',
// http://www.pyleaudio.com/Products.aspx?%2fproducts%2fPersonal-Electronics%2fTablets
'PyleAudioTablet' => '\b(PTBL10CEU|PTBL10C|PTBL72BC|PTBL72BCEU|PTBL7CEU|PTBL7C|PTBL92BC|PTBL92BCEU|PTBL9CEU|PTBL9CUK|PTBL9C)\b',
// http://www.advandigital.com/index.php?link=content-product&jns=JP001
// because of the short codenames we have to include whitespaces to reduce the possible conflicts.
'AdvanTablet' => 'Android.* \b(E3A|T3X|T5C|T5B|T3E|T3C|T3B|T1J|T1F|T2A|T1H|T1i|E1C|T1-E|T5-A|T4|E1-B|T2Ci|T1-B|T1-D|O1-A|E1-A|T1-A|T3A|T4i)\b ',
// http://www.danytech.com/category/tablet-pc
'DanyTechTablet' => 'Genius Tab G3|Genius Tab S2|Genius Tab Q3|Genius Tab G4|Genius Tab Q4|Genius Tab G-II|Genius TAB GII|Genius TAB GIII|Genius Tab S1',
// http://www.galapad.net/product.html ; https://github.com/serbanghita/Mobile-Detect/issues/761
'GalapadTablet' => 'Android [0-9.]+; [a-z-]+; \bG1\b',
// http://www.micromaxinfo.com/tablet/funbook
'MicromaxTablet' => 'Funbook|Micromax.*\b(P250|P560|P360|P362|P600|P300|P350|P500|P275)\b',
// http://www.karbonnmobiles.com/products_tablet.php
'KarbonnTablet' => 'Android.*\b(A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2)\b',
// http://www.myallfine.com/Products.asp
'AllFineTablet' => 'Fine7 Genius|Fine7 Shine|Fine7 Air|Fine8 Style|Fine9 More|Fine10 Joy|Fine11 Wide',
// http://www.proscanvideo.com/products-search.asp?itemClass=TABLET&itemnmbr=
'PROSCANTablet' => '\b(PEM63|PLT1023G|PLT1041|PLT1044|PLT1044G|PLT1091|PLT4311|PLT4311PL|PLT4315|PLT7030|PLT7033|PLT7033D|PLT7035|PLT7035D|PLT7044K|PLT7045K|PLT7045KB|PLT7071KG|PLT7072|PLT7223G|PLT7225G|PLT7777G|PLT7810K|PLT7849G|PLT7851G|PLT7852G|PLT8015|PLT8031|PLT8034|PLT8036|PLT8080K|PLT8082|PLT8088|PLT8223G|PLT8234G|PLT8235G|PLT8816K|PLT9011|PLT9045K|PLT9233G|PLT9735|PLT9760G|PLT9770G)\b',
// http://www.yonesnav.com/products/products.php
'YONESTablet' => 'BQ1078|BC1003|BC1077|RK9702|BC9730|BC9001|IT9001|BC7008|BC7010|BC708|BC728|BC7012|BC7030|BC7027|BC7026',
// http://www.cjshowroom.com/eproducts.aspx?classcode=004001001
// China manufacturer makes tablets for different small brands (eg. http://www.zeepad.net/index.html)
'ChangJiaTablet' => 'TPC7102|TPC7103|TPC7105|TPC7106|TPC7107|TPC7201|TPC7203|TPC7205|TPC7210|TPC7708|TPC7709|TPC7712|TPC7110|TPC8101|TPC8103|TPC8105|TPC8106|TPC8203|TPC8205|TPC8503|TPC9106|TPC9701|TPC97101|TPC97103|TPC97105|TPC97106|TPC97111|TPC97113|TPC97203|TPC97603|TPC97809|TPC97205|TPC10101|TPC10103|TPC10106|TPC10111|TPC10203|TPC10205|TPC10503',
// http://www.gloryunion.cn/products.asp
// http://www.allwinnertech.com/en/apply/mobile.html
// http://www.ptcl.com.pk/pd_content.php?pd_id=284 (EVOTAB)
// @todo: Softwiner tablets?
// aka. Cute or Cool tablets. Not sure yet, must research to avoid collisions.
'GUTablet' => 'TX-A1301|TX-M9002|Q702|kf026', // A12R|D75A|D77|D79|R83|A95|A106C|R15|A75|A76|D71|D72|R71|R73|R77|D82|R85|D92|A97|D92|R91|A10F|A77F|W71F|A78F|W78F|W81F|A97F|W91F|W97F|R16G|C72|C73E|K72|K73|R96G
// http://www.pointofview-online.com/showroom.php?shop_mode=product_listing&category_id=118
'PointOfViewTablet' => 'TAB-P506|TAB-navi-7-3G-M|TAB-P517|TAB-P-527|TAB-P701|TAB-P703|TAB-P721|TAB-P731N|TAB-P741|TAB-P825|TAB-P905|TAB-P925|TAB-PR945|TAB-PL1015|TAB-P1025|TAB-PI1045|TAB-P1325|TAB-PROTAB[0-9]+|TAB-PROTAB25|TAB-PROTAB26|TAB-PROTAB27|TAB-PROTAB26XL|TAB-PROTAB2-IPS9|TAB-PROTAB30-IPS9|TAB-PROTAB25XXL|TAB-PROTAB26-IPS10|TAB-PROTAB30-IPS10',
// http://www.overmax.pl/pl/katalog-produktow,p8/tablety,c14/
// @todo: add more tests.
'OvermaxTablet' => 'OV-(SteelCore|NewBase|Basecore|Baseone|Exellen|Quattor|EduTab|Solution|ACTION|BasicTab|TeddyTab|MagicTab|Stream|TB-08|TB-09)|Qualcore 1027',
// http://hclmetablet.com/India/index.php
'HCLTablet' => 'HCL.*Tablet|Connect-3G-2.0|Connect-2G-2.0|ME Tablet U1|ME Tablet U2|ME Tablet G1|ME Tablet X1|ME Tablet Y2|ME Tablet Sync',
// http://www.edigital.hu/Tablet_es_e-book_olvaso/Tablet-c18385.html
'DPSTablet' => 'DPS Dream 9|DPS Dual 7',
// http://www.visture.com/index.asp
'VistureTablet' => 'V97 HD|i75 3G|Visture V4( HD)?|Visture V5( HD)?|Visture V10',
// http://www.mijncresta.nl/tablet
'CrestaTablet' => 'CTP(-)?810|CTP(-)?818|CTP(-)?828|CTP(-)?838|CTP(-)?888|CTP(-)?978|CTP(-)?980|CTP(-)?987|CTP(-)?988|CTP(-)?989',
// MediaTek - http://www.mediatek.com/_en/01_products/02_proSys.php?cata_sn=1&cata1_sn=1&cata2_sn=309
'MediatekTablet' => '\bMT8125|MT8389|MT8135|MT8377\b',
// Concorde tab
'ConcordeTablet' => 'Concorde([ ]+)?Tab|ConCorde ReadMan',
// GoClever Tablets - http://www.goclever.com/uk/products,c1/tablet,c5/
'GoCleverTablet' => 'GOCLEVER TAB|A7GOCLEVER|M1042|M7841|M742|R1042BK|R1041|TAB A975|TAB A7842|TAB A741|TAB A741L|TAB M723G|TAB M721|TAB A1021|TAB I921|TAB R721|TAB I720|TAB T76|TAB R70|TAB R76.2|TAB R106|TAB R83.2|TAB M813G|TAB I721|GCTA722|TAB I70|TAB I71|TAB S73|TAB R73|TAB R74|TAB R93|TAB R75|TAB R76.1|TAB A73|TAB A93|TAB A93.2|TAB T72|TAB R83|TAB R974|TAB R973|TAB A101|TAB A103|TAB A104|TAB A104.2|R105BK|M713G|A972BK|TAB A971|TAB R974.2|TAB R104|TAB R83.3|TAB A1042',
// Modecom Tablets - http://www.modecom.eu/tablets/portal/
'ModecomTablet' => 'FreeTAB 9000|FreeTAB 7.4|FreeTAB 7004|FreeTAB 7800|FreeTAB 2096|FreeTAB 7.5|FreeTAB 1014|FreeTAB 1001 |FreeTAB 8001|FreeTAB 9706|FreeTAB 9702|FreeTAB 7003|FreeTAB 7002|FreeTAB 1002|FreeTAB 7801|FreeTAB 1331|FreeTAB 1004|FreeTAB 8002|FreeTAB 8014|FreeTAB 9704|FreeTAB 1003',
// Vonino Tablets
'VoninoTablet' => '\b(Argus[ _]?S|Diamond[ _]?79HD|Emerald[ _]?78E|Luna[ _]?70C|Onyx[ _]?S|Onyx[ _]?Z|Orin[ _]?HD|Orin[ _]?S|Otis[ _]?S|SpeedStar[ _]?S|Magnet[ _]?M9|Primus[ _]?94[ _]?3G|Primus[ _]?94HD|Primus[ _]?QS|Android.*\bQ8\b|Sirius[ _]?EVO[ _]?QS|Sirius[ _]?QS|Spirit[ _]?S)\b',
// ECS Tablets - http://www.ecs.com.tw/ECSWebSite/Product/Product_Tablet_List.aspx?CategoryID=14&MenuID=107&childid=M_107&LanID=0
'ECSTablet' => 'V07OT2|TM105A|S10OT1|TR10CS1',
// Storex Tablets - http://storex.fr/espace_client/support.html
// @note: no need to add all the tablet codes since they are guided by the first regex.
'StorexTablet' => 'eZee[_\']?(Tab|Go)[0-9]+|TabLC7|Looney Tunes Tab',
// Generic Vodafone tablets.
'VodafoneTablet' => 'SmartTab([ ]+)?[0-9]+|SmartTabII10|SmartTabII7|VF-1497|VFD 1400',
// French tablets - Essentiel B http://www.boulanger.fr/tablette_tactile_e-book/tablette_tactile_essentiel_b/cl_68908.htm?multiChoiceToDelete=brand&mc_brand=essentielb
// Aka: http://www.essentielb.fr/
'EssentielBTablet' => 'Smart[ \']?TAB[ ]+?[0-9]+|Family[ \']?TAB2',
// Ross & Moor - http://ross-moor.ru/
'RossMoorTablet' => 'RM-790|RM-997|RMD-878G|RMD-974R|RMT-705A|RMT-701|RME-601|RMT-501|RMT-711',
// i-mobile http://product.i-mobilephone.com/Mobile_Device
'iMobileTablet' => 'i-mobile i-note',
// http://www.tolino.de/de/vergleichen/
'TolinoTablet' => 'tolino tab [0-9.]+|tolino shine',
// AudioSonic - a Kmart brand
// http://www.kmart.com.au/webapp/wcs/stores/servlet/Search?langId=-1&storeId=10701&catalogId=10001&categoryId=193001&pageSize=72¤tPage=1&searchCategory=193001%2b4294965664&sortBy=p_MaxPrice%7c1
'AudioSonicTablet' => '\bC-22Q|T7-QC|T-17B|T-17P\b',
// AMPE Tablets - http://www.ampe.com.my/product-category/tablets/
// @todo: add them gradually to avoid conflicts.
'AMPETablet' => 'Android.* A78 ',
// Skk Mobile - http://skkmobile.com.ph/product_tablets.php
'SkkTablet' => 'Android.* (SKYPAD|PHOENIX|CYCLOPS)',
// Tecno Mobile (only tablet) - http://www.tecno-mobile.com/index.php/product?filterby=smart&list_order=all&page=1
'TecnoTablet' => 'TECNO P9|TECNO DP8D',
// JXD (consoles & tablets) - http://jxd.hk/products.asp?selectclassid=009008&clsid=3
'JXDTablet' => 'Android.* \b(F3000|A3300|JXD5000|JXD3000|JXD2000|JXD300B|JXD300|S5800|S7800|S602b|S5110b|S7300|S5300|S602|S603|S5100|S5110|S601|S7100a|P3000F|P3000s|P101|P200s|P1000m|P200m|P9100|P1000s|S6600b|S908|P1000|P300|S18|S6600|S9100)\b',
// i-Joy tablets - http://www.i-joy.es/en/cat/products/tablets/
'iJoyTablet' => 'Tablet (Spirit 7|Essentia|Galatea|Fusion|Onix 7|Landa|Titan|Scooby|Deox|Stella|Themis|Argon|Unique 7|Sygnus|Hexen|Finity 7|Cream|Cream X2|Jade|Neon 7|Neron 7|Kandy|Scape|Saphyr 7|Rebel|Biox|Rebel|Rebel 8GB|Myst|Draco 7|Myst|Tab7-004|Myst|Tadeo Jones|Tablet Boing|Arrow|Draco Dual Cam|Aurix|Mint|Amity|Revolution|Finity 9|Neon 9|T9w|Amity 4GB Dual Cam|Stone 4GB|Stone 8GB|Andromeda|Silken|X2|Andromeda II|Halley|Flame|Saphyr 9,7|Touch 8|Planet|Triton|Unique 10|Hexen 10|Memphis 4GB|Memphis 8GB|Onix 10)',
// http://www.intracon.eu/tablet
'FX2Tablet' => 'FX2 PAD7|FX2 PAD10',
// http://www.xoro.de/produkte/
// @note: Might be the same brand with 'Simply tablets'
'XoroTablet' => 'KidsPAD 701|PAD[ ]?712|PAD[ ]?714|PAD[ ]?716|PAD[ ]?717|PAD[ ]?718|PAD[ ]?720|PAD[ ]?721|PAD[ ]?722|PAD[ ]?790|PAD[ ]?792|PAD[ ]?900|PAD[ ]?9715D|PAD[ ]?9716DR|PAD[ ]?9718DR|PAD[ ]?9719QR|PAD[ ]?9720QR|TelePAD1030|Telepad1032|TelePAD730|TelePAD731|TelePAD732|TelePAD735Q|TelePAD830|TelePAD9730|TelePAD795|MegaPAD 1331|MegaPAD 1851|MegaPAD 2151',
// http://www1.viewsonic.com/products/computing/tablets/
'ViewsonicTablet' => 'ViewPad 10pi|ViewPad 10e|ViewPad 10s|ViewPad E72|ViewPad7|ViewPad E100|ViewPad 7e|ViewSonic VB733|VB100a',
// https://www.verizonwireless.com/tablets/verizon/
'VerizonTablet' => 'QTAQZ3|QTAIR7|QTAQTZ3|QTASUN1|QTASUN2|QTAXIA1',
// http://www.odys.de/web/internet-tablet_en.html
'OdysTablet' => 'LOOX|XENO10|ODYS[ -](Space|EVO|Xpress|NOON)|\bXELIO\b|Xelio10Pro|XELIO7PHONETAB|XELIO10EXTREME|XELIOPT2|NEO_QUAD10',
// http://www.captiva-power.de/products.html#tablets-en
'CaptivaTablet' => 'CAPTIVA PAD',
// IconBIT - http://www.iconbit.com/products/tablets/
'IconbitTablet' => 'NetTAB|NT-3702|NT-3702S|NT-3702S|NT-3603P|NT-3603P|NT-0704S|NT-0704S|NT-3805C|NT-3805C|NT-0806C|NT-0806C|NT-0909T|NT-0909T|NT-0907S|NT-0907S|NT-0902S|NT-0902S',
// http://www.teclast.com/topic.php?channelID=70&topicID=140&pid=63
'TeclastTablet' => 'T98 4G|\bP80\b|\bX90HD\b|X98 Air|X98 Air 3G|\bX89\b|P80 3G|\bX80h\b|P98 Air|\bX89HD\b|P98 3G|\bP90HD\b|P89 3G|X98 3G|\bP70h\b|P79HD 3G|G18d 3G|\bP79HD\b|\bP89s\b|\bA88\b|\bP10HD\b|\bP19HD\b|G18 3G|\bP78HD\b|\bA78\b|\bP75\b|G17s 3G|G17h 3G|\bP85t\b|\bP90\b|\bP11\b|\bP98t\b|\bP98HD\b|\bG18d\b|\bP85s\b|\bP11HD\b|\bP88s\b|\bA80HD\b|\bA80se\b|\bA10h\b|\bP89\b|\bP78s\b|\bG18\b|\bP85\b|\bA70h\b|\bA70\b|\bG17\b|\bP18\b|\bA80s\b|\bA11s\b|\bP88HD\b|\bA80h\b|\bP76s\b|\bP76h\b|\bP98\b|\bA10HD\b|\bP78\b|\bP88\b|\bA11\b|\bA10t\b|\bP76a\b|\bP76t\b|\bP76e\b|\bP85HD\b|\bP85a\b|\bP86\b|\bP75HD\b|\bP76v\b|\bA12\b|\bP75a\b|\bA15\b|\bP76Ti\b|\bP81HD\b|\bA10\b|\bT760VE\b|\bT720HD\b|\bP76\b|\bP73\b|\bP71\b|\bP72\b|\bT720SE\b|\bC520Ti\b|\bT760\b|\bT720VE\b|T720-3GE|T720-WiFi',
// Onda - http://www.onda-tablet.com/buy-android-onda.html?dir=desc&limit=all&order=price
'OndaTablet' => '\b(V975i|Vi30|VX530|V701|Vi60|V701s|Vi50|V801s|V719|Vx610w|VX610W|V819i|Vi10|VX580W|Vi10|V711s|V813|V811|V820w|V820|Vi20|V711|VI30W|V712|V891w|V972|V819w|V820w|Vi60|V820w|V711|V813s|V801|V819|V975s|V801|V819|V819|V818|V811|V712|V975m|V101w|V961w|V812|V818|V971|V971s|V919|V989|V116w|V102w|V973|Vi40)\b[\s]+|V10 \b4G\b',
'JaytechTablet' => 'TPC-PA762',
'BlaupunktTablet' => 'Endeavour 800NG|Endeavour 1010',
// http://www.digma.ru/support/download/
// @todo: Ebooks also (if requested)
'DigmaTablet' => '\b(iDx10|iDx9|iDx8|iDx7|iDxD7|iDxD8|iDsQ8|iDsQ7|iDsQ8|iDsD10|iDnD7|3TS804H|iDsQ11|iDj7|iDs10)\b',
// http://www.evolioshop.com/ro/tablete-pc.html
// http://www.evolio.ro/support/downloads_static.html?cat=2
// @todo: Research some more
'EvolioTablet' => 'ARIA_Mini_wifi|Aria[ _]Mini|Evolio X10|Evolio X7|Evolio X8|\bEvotab\b|\bNeura\b',
// @todo http://www.lavamobiles.com/tablets-data-cards
'LavaTablet' => 'QPAD E704|\bIvoryS\b|E-TAB IVORY|\bE-TAB\b',
// http://www.breezetablet.com/
'AocTablet' => 'MW0811|MW0812|MW0922|MTK8382|MW1031|MW0831|MW0821|MW0931|MW0712',
// http://www.mpmaneurope.com/en/products/internet-tablets-14/android-tablets-14/
'MpmanTablet' => 'MP11 OCTA|MP10 OCTA|MPQC1114|MPQC1004|MPQC994|MPQC974|MPQC973|MPQC804|MPQC784|MPQC780|\bMPG7\b|MPDCG75|MPDCG71|MPDC1006|MP101DC|MPDC9000|MPDC905|MPDC706HD|MPDC706|MPDC705|MPDC110|MPDC100|MPDC99|MPDC97|MPDC88|MPDC8|MPDC77|MP709|MID701|MID711|MID170|MPDC703|MPQC1010',
// https://www.celkonmobiles.com/?_a=categoryphones&sid=2
'CelkonTablet' => 'CT695|CT888|CT[\s]?910|CT7 Tab|CT9 Tab|CT3 Tab|CT2 Tab|CT1 Tab|C820|C720|\bCT-1\b',
// http://www.wolderelectronics.com/productos/manuales-y-guias-rapidas/categoria-2-miTab
'WolderTablet' => 'miTab \b(DIAMOND|SPACE|BROOKLYN|NEO|FLY|MANHATTAN|FUNK|EVOLUTION|SKY|GOCAR|IRON|GENIUS|POP|MINT|EPSILON|BROADWAY|JUMP|HOP|LEGEND|NEW AGE|LINE|ADVANCE|FEEL|FOLLOW|LIKE|LINK|LIVE|THINK|FREEDOM|CHICAGO|CLEVELAND|BALTIMORE-GH|IOWA|BOSTON|SEATTLE|PHOENIX|DALLAS|IN 101|MasterChef)\b',
'MediacomTablet' => 'M-MPI10C3G|M-SP10EG|M-SP10EGP|M-SP10HXAH|M-SP7HXAH|M-SP10HXBH|M-SP8HXAH|M-SP8MXA',
// http://www.mi.com/en
'MiTablet' => '\bMI PAD\b|\bHM NOTE 1W\b',
// http://www.nbru.cn/index.html
'NibiruTablet' => 'Nibiru M1|Nibiru Jupiter One',
// http://navroad.com/products/produkty/tablety/
// http://navroad.com/products/produkty/tablety/
'NexoTablet' => 'NEXO NOVA|NEXO 10|NEXO AVIO|NEXO FREE|NEXO GO|NEXO EVO|NEXO 3G|NEXO SMART|NEXO KIDDO|NEXO MOBI',
// http://leader-online.com/new_site/product-category/tablets/
// http://www.leader-online.net.au/List/Tablet
'LeaderTablet' => 'TBLT10Q|TBLT10I|TBL-10WDKB|TBL-10WDKBO2013|TBL-W230V2|TBL-W450|TBL-W500|SV572|TBLT7I|TBA-AC7-8G|TBLT79|TBL-8W16|TBL-10W32|TBL-10WKB|TBL-W100',
// http://www.datawind.com/ubislate/
'UbislateTablet' => 'UbiSlate[\s]?7C',
// http://www.pocketbook-int.com/ru/support
'PocketBookTablet' => 'Pocketbook',
// http://www.kocaso.com/product_tablet.html
'KocasoTablet' => '\b(TB-1207)\b',
// http://global.hisense.com/product/asia/tablet/Sero7/201412/t20141215_91832.htm
'HisenseTablet' => '\b(F5281|E2371)\b',
// http://www.tesco.com/direct/hudl/
'Hudl' => 'Hudl HT7S3|Hudl 2',
// http://www.telstra.com.au/home-phone/thub-2/
'TelstraTablet' => 'T-Hub2',
'GenericTablet' => 'Android.*\b97D\b|Tablet(?!.*PC)|BNTV250A|MID-WCDMA|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b|rk30sdk|\bEVOTAB\b|M758A|ET904|ALUMIUM10|Smartfren Tab|Endeavour 1010|Tablet-PC-4|Tagi Tab|\bM6pro\b|CT1020W|arc 10HD|\bTP750\b|\bQTAQZ3\b|WVT101|TM1088|KT107'
];
/**
* List of mobile Operating Systems.
*
* @var array
*/
protected static array $operatingSystems = [
'AndroidOS' => 'Android',
'BlackBerryOS' => 'blackberry|\bBB10\b|rim tablet os',
'PalmOS' => 'PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino',
'SymbianOS' => 'Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\bS60\b',
// @reference: http://en.wikipedia.org/wiki/Windows_Mobile
'WindowsMobileOS' => 'Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Windows Mobile|Windows Phone [0-9.]+|WCE;',
// @reference: http://en.wikipedia.org/wiki/Windows_Phone
// http://wifeng.cn/?r=blog&a=view&id=106
// http://nicksnettravels.builttoroam.com/post/2011/01/10/Bogus-Windows-Phone-7-User-Agent-String.aspx
// http://msdn.microsoft.com/library/ms537503.aspx
// https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
'WindowsPhoneOS' => 'Windows Phone 10.0|Windows Phone 8.1|Windows Phone 8.0|Windows Phone OS|XBLWP7|ZuneWP7|Windows NT 6.[23]; ARM;',
'iOS' => '\biPhone.*Mobile|\biPod|\biPad|AppleCoreMedia',
// https://en.wikipedia.org/wiki/IPadOS
'iPadOS' => 'CPU OS 13',
// @reference https://en.m.wikipedia.org/wiki/Sailfish_OS
// https://sailfishos.org/
'SailfishOS' => 'Sailfish',
// http://en.wikipedia.org/wiki/MeeGo
// @todo: research MeeGo in UAs
'MeeGoOS' => 'MeeGo',
// http://en.wikipedia.org/wiki/Maemo
// @todo: research Maemo in UAs
'MaemoOS' => 'Maemo',
'JavaOS' => 'J2ME/|\bMIDP\b|\bCLDC\b', // '|Java/' produces bug #135
'webOS' => 'webOS|hpwOS',
'badaOS' => '\bBada\b',
'BREWOS' => 'BREW',
];
/**
* List of mobile User Agents.
*
* IMPORTANT: This is a list of only mobile browsers.
* Mobile Detect 2.x supports only mobile browsers,
* it was never designed to detect all browsers.
* The change will come in 2017 in the 3.x release for PHP7.
*
* @var array
*/
protected static array $browsers = [
//'Vivaldi' => 'Vivaldi',
// @reference: https://developers.google.com/chrome/mobile/docs/user-agent
'Chrome' => '\bCrMo\b|CriOS.*Mobile|Android.*Chrome/[.0-9]* Mobile',
'Dolfin' => '\bDolfin\b',
'Opera' => 'Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR/[0-9.]+$|Coast/[0-9.]+',
'Skyfire' => 'Skyfire',
// Added "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764
'Edge' => 'EdgiOS.*Mobile|Mobile Safari/[.0-9]* Edge',
'IE' => 'IEMobile|MSIEMobile', // |Trident/[.0-9]+
'Firefox' => 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile|FxiOS.*Mobile',
'Bolt' => 'bolt',
'TeaShark' => 'teashark',
'Blazer' => 'Blazer',
// @reference: http://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/OptimizingforSafarioniPhone/OptimizingforSafarioniPhone.html#//apple_ref/doc/uid/TP40006517-SW3
// Excluded "Edge on iOS" https://github.com/serbanghita/Mobile-Detect/issues/764
'Safari' => 'Version((?!\bEdgiOS\b).)*Mobile.*Safari|Safari.*Mobile|MobileSafari',
// http://en.wikipedia.org/wiki/Midori_(web_browser)
//'Midori' => 'midori',
//'Tizen' => 'Tizen',
'WeChat' => '\bMicroMessenger\b',
'UCBrowser' => 'UC.*Browser|UCWEB',
'baiduboxapp' => 'baiduboxapp',
'baidubrowser' => 'baidubrowser',
// https://github.com/serbanghita/Mobile-Detect/issues/7
'DiigoBrowser' => 'DiigoBrowser',
// http://www.puffinbrowser.com/index.php
// https://github.com/serbanghita/Mobile-Detect/issues/752
// 'Puffin' => 'Puffin',
// http://mercury-browser.com/index.html
'Mercury' => '\bMercury\b',
// http://en.wikipedia.org/wiki/Obigo_Browser
'ObigoBrowser' => 'Obigo',
// http://en.wikipedia.org/wiki/NetFront
'NetFront' => 'NF-Browser',
// @reference: http://en.wikipedia.org/wiki/Minimo
// http://en.wikipedia.org/wiki/Vision_Mobile_Browser
'GenericBrowser' => 'NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision|MQQBrowser|MicroMessenger',
// @reference: https://en.wikipedia.org/wiki/Pale_Moon_(web_browser)
'PaleMoon' => 'Android.*PaleMoon|Mobile.*PaleMoon',
];
/**
* All possible HTTP headers that represent the
* User-Agent string.
*
* @var array
*/
protected static array $uaHttpHeaders = [
// The default User-Agent string.
'HTTP_USER_AGENT',
// Header can occur on devices using Opera Mini.
'HTTP_X_OPERAMINI_PHONE_UA',
// Vodafone specific header: http://www.seoprinciple.com/mobile-web-community-still-angry-at-vodafone/24/
'HTTP_X_DEVICE_USER_AGENT',
'HTTP_X_ORIGINAL_USER_AGENT',
'HTTP_X_SKYFIRE_PHONE',
'HTTP_X_BOLT_PHONE_UA',
'HTTP_DEVICE_STOCK_UA',
'HTTP_X_UCBROWSER_DEVICE_UA'
];
/**
* The individual segments that could exist in a User-Agent string. VER refers to the regular
* expression defined in the constant self::VER.
*
* @var array
*/
protected static array $properties = [
// Build
'Mobile' => 'Mobile/[VER]',
'Build' => 'Build/[VER]',
'Version' => 'Version/[VER]',
'VendorID' => 'VendorID/[VER]',
// Devices
'iPad' => 'iPad.*CPU[a-z ]+[VER]',
'iPhone' => 'iPhone.*CPU[a-z ]+[VER]',
'iPod' => 'iPod.*CPU[a-z ]+[VER]',
//'BlackBerry' => array('BlackBerry[VER]', 'BlackBerry [VER];'),
'Kindle' => 'Kindle/[VER]',
// Browser
'Chrome' => ['Chrome/[VER]', 'CriOS/[VER]', 'CrMo/[VER]'],
'Coast' => ['Coast/[VER]'],
'Dolfin' => 'Dolfin/[VER]',
// @reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox
'Firefox' => ['Firefox/[VER]', 'FxiOS/[VER]'],
'Fennec' => 'Fennec/[VER]',
// http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/ie/hh869301(v=vs.85).aspx
'Edge' => 'Edge/[VER]',
'IE' => ['IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'Trident/[0-9.]+;.*rv:[VER]'],
// http://en.wikipedia.org/wiki/NetFront
'NetFront' => 'NetFront/[VER]',
'NokiaBrowser' => 'NokiaBrowser/[VER]',
'Opera' => [' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]'],
'Opera Mini' => 'Opera Mini/[VER]',
'Opera Mobi' => 'Version/[VER]',
'UCBrowser' => ['UCWEB[VER]', 'UC.*Browser/[VER]'],
'MQQBrowser' => 'MQQBrowser/[VER]',
'MicroMessenger' => 'MicroMessenger/[VER]',
'baiduboxapp' => 'baiduboxapp/[VER]',
'baidubrowser' => 'baidubrowser/[VER]',
'SamsungBrowser' => 'SamsungBrowser/[VER]',
'Iron' => 'Iron/[VER]',
// @note: Safari 7534.48.3 is actually Version 5.1.
// @note: On BlackBerry the Version is overwriten by the OS.
'Safari' => ['Version/[VER]', 'Safari/[VER]'],
'Skyfire' => 'Skyfire/[VER]',
'Tizen' => 'Tizen/[VER]',
'Webkit' => 'webkit[ /][VER]',
'PaleMoon' => 'PaleMoon/[VER]',
'SailfishBrowser' => 'SailfishBrowser/[VER]',
// Engine
'Gecko' => 'Gecko/[VER]',
'Trident' => 'Trident/[VER]',
'Presto' => 'Presto/[VER]',
'Goanna' => 'Goanna/[VER]',
// OS
'iOS' => ' \bi?OS\b [VER][ ;]{1}',
'Android' => 'Android [VER]',
'Sailfish' => 'Sailfish [VER]',
'BlackBerry' => ['BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'],
'BREW' => 'BREW [VER]',
'Java' => 'Java/[VER]',
// @reference: http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/08/29/introducing-the-ie9-on-windows-phone-mango-user-agent-string.aspx
// @reference: http://en.wikipedia.org/wiki/Windows_NT#Releases
'Windows Phone OS' => ['Windows Phone OS [VER]', 'Windows Phone [VER]'],
'Windows Phone' => 'Windows Phone [VER]',
'Windows CE' => 'Windows CE/[VER]',
// http://social.msdn.microsoft.com/Forums/en-US/windowsdeveloperpreviewgeneral/thread/6be392da-4d2f-41b4-8354-8dcee20c85cd
'Windows NT' => 'Windows NT [VER]',
'Symbian' => ['SymbianOS/[VER]', 'Symbian/[VER]'],
'webOS' => ['webOS/[VER]', 'hpwOS/[VER];'],
];
/**
* Construct an instance of this class.
*
* @param array|null $headers Specify the headers as injection. Should be PHP _SERVER flavored.
* If left empty, will use the global _SERVER['HTTP_*'] vars instead.
* @param null $userAgent Inject the User-Agent header. If null, will use HTTP_USER_AGENT
* from the $headers array instead.
*/
public function __construct(array $headers = null, $userAgent = null)
{
$this->setHttpHeaders($headers);
$this->setUserAgent($userAgent);
}
/**
* Get the current script version.
* This is useful for the demo.php file,
* so people can check on what version they are testing
* for mobile devices.
*
* @return string The version number in semantic version format.
*/
public static function getScriptVersion(): string
{
return self::VERSION;
}
/**
* Set the HTTP Headers. Must be PHP-flavored. This method will reset existing headers.
*
* @param array|null $httpHeaders The headers to set. If null, then using PHP's _SERVER to extract
* the headers. The default null is left for backwards compatibility.
*/
public function setHttpHeaders(array $httpHeaders = null)
{
// use global _SERVER if $httpHeaders aren't defined
if (!is_array($httpHeaders) || !count($httpHeaders)) {
$httpHeaders = $_SERVER;
}
// clear existing headers
$this->httpHeaders = array();
// Only save HTTP headers. In PHP land, that means only _SERVER vars that
// start with HTTP_.
foreach ($httpHeaders as $key => $value) {
if (substr($key, 0, 5) === 'HTTP_') {
$this->httpHeaders[$key] = $value;
}
}
// In case we're dealing with CloudFront, we need to know.
$this->setCfHeaders($httpHeaders);
}
/**
* Retrieves the HTTP headers.
*
* @return array
*/
public function getHttpHeaders(): array
{
return $this->httpHeaders;
}
/**
* Retrieves a particular header. If it doesn't exist, no exception/error is caused.
* Simply null is returned.
*
* @param string $header The name of the header to retrieve. Can be HTTP compliant such as
* "User-Agent" or "X-Device-User-Agent" or can be php-esque with the
* all-caps, HTTP_ prefixed, underscore separated awesomeness.
*
* @return string|null The value of the header.
*/
public function getHttpHeader(string $header): ?string
{
// are we using PHP-flavored headers?
if (strpos($header, '_') === false) {
$header = str_replace('-', '_', $header);
$header = strtoupper($header);
}
// test the alternate, too
$altHeader = 'HTTP_' . $header;
//Test both the regular and the HTTP_ prefix
if (isset($this->httpHeaders[$header])) {
return $this->httpHeaders[$header];
} elseif (isset($this->httpHeaders[$altHeader])) {
return $this->httpHeaders[$altHeader];
}
return null;
}
public function getMobileHeaders(): array
{
return self::$mobileHeaders;
}
/**
* Get all possible HTTP headers that
* can contain the User-Agent string.
*
* @return array List of HTTP headers.
*/
public function getUaHttpHeaders(): array
{
return self::$uaHttpHeaders;
}
/**
* Set CloudFront headers
* http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device
*
* @param array|null $cfHeaders List of HTTP headers
*
* @return boolean If there were CloudFront headers to be set
*/
public function setCfHeaders(array $cfHeaders = null): bool
{
// use global _SERVER if $cfHeaders aren't defined
if (!is_array($cfHeaders) || !count($cfHeaders)) {
$cfHeaders = $_SERVER;
}
// clear existing headers
$this->cloudfrontHeaders = array();
// Only save CLOUDFRONT headers. In PHP land, that means only _SERVER vars that
// start with cloudfront-.
$response = false;
foreach ($cfHeaders as $key => $value) {
if (substr(strtolower($key), 0, 16) === 'http_cloudfront_') {
$this->cloudfrontHeaders[strtoupper($key)] = $value;
$response = true;
}
}
return $response;
}
/**
* Retrieves the cloudfront headers.
*
* @return array
*/
public function getCfHeaders(): array
{
return $this->cloudfrontHeaders;
}
/**
* @param string $userAgent
* @return string
*/
private function prepareUserAgent(string $userAgent): string
{
$userAgent = trim($userAgent);
return substr($userAgent, 0, 500);
}
/**
* Set the User-Agent to be used.
*
* @param string|null $userAgent The user agent string to set.
*
* @return string|null
*/
public function setUserAgent(string $userAgent = null): ?string
{
// Invalidate cache due to #375
$this->cache = array();
if (false === empty($userAgent)) {
return $this->userAgent = $this->prepareUserAgent($userAgent);
} else {
$this->userAgent = null;
foreach ($this->getUaHttpHeaders() as $altHeader) {
// @todo: should use getHttpHeader(), but it would be slow. (Serban)
if (false === empty($this->httpHeaders[$altHeader])) {
$this->userAgent .= $this->httpHeaders[$altHeader] . " ";
}
}
if (!empty($this->userAgent)) {
return $this->userAgent = $this->prepareUserAgent($this->userAgent);
}
}
if (count($this->getCfHeaders()) > 0) {
return $this->userAgent = 'Amazon CloudFront';
}
return $this->userAgent = null;
}
/**
* Retrieve the User-Agent.
*
* @return string|null The user agent if it's set.
*/
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getMatchingRegex(): ?string
{
return $this->matchingRegex;
}
public function getMatchesArray(): ?string
{
return $this->matchesArray;
}
/**
* Retrieve the list of known phone devices.
*
* @return array List of phone devices.
*/
public static function getPhoneDevices(): array
{
return self::$phoneDevices;
}
/**
* Retrieve the list of known tablet devices.
*
* @return array List of tablet devices.
*/
public static function getTabletDevices(): array
{
return self::$tabletDevices;
}
/**
* Alias for getBrowsers() method.
*
* @return array List of user agents.
*/
public static function getUserAgents(): array
{
return self::getBrowsers();
}
/**
* Retrieve the list of known browsers. Specifically, the user agents.
*
* @return array List of browsers / user agents.
*/
public static function getBrowsers(): array
{
return self::$browsers;
}
/**
* Method gets the mobile detection rules. This method is used for the magic methods $detect->is*().
* Retrieve the current set of rules.
*
* @return array
*/
public function getRules(): array
{
static $rules;
if (!$rules) {
$rules = array_merge(
self::$phoneDevices,
self::$tabletDevices,
self::$operatingSystems,
self::$browsers
);
}
return $rules;
}
/**
* Retrieve the list of mobile operating systems.
*
* @return array The list of mobile operating systems.
*/
public static function getOperatingSystems(): array
{
return self::$operatingSystems;
}
/**
* Check the HTTP headers for signs of mobile.
* This is the fastest mobile check possible; it's used
* inside isMobile() method.
*
* @return bool
*/
public function checkHttpHeadersForMobile(): bool
{
foreach ($this->getMobileHeaders() as $mobileHeader => $matchType) {
if (isset($this->httpHeaders[$mobileHeader])) {
if (isset($matchType['matches']) && is_array($matchType['matches'])) {
foreach ($matchType['matches'] as $_match) {
if (strpos($this->httpHeaders[$mobileHeader], $_match) !== false) {
return true;
}
}
return false;
} else {
return true;
}
}
}
return false;
}
/**
* Magic overloading method.
*
* @method boolean is[...]()
* @param string $name
* @param array $arguments
* @return bool
* @throws BadMethodCallException when the method doesn't exist and doesn't start with 'is'
*/
public function __call(string $name, array $arguments)
{
// make sure the name starts with 'is', otherwise
if (substr($name, 0, 2) !== 'is') {
throw new BadMethodCallException("No such method exists: $name");
}
$key = substr($name, 2);
return $this->matchUAAgainstKey($key);
}
/**
* Find a detection rule that matches the current User-agent.
*
* @param null $userAgent deprecated
* @return boolean
*/
protected function matchDetectionRulesAgainstUA($userAgent = null): bool
{
// Begin general search.
foreach ($this->getRules() as $_regex) {
if (empty($_regex)) {
continue;
}
if ($this->match($_regex, $userAgent)) {
return true;
}
}
return false;
}
/**
* Search for a certain key in the rules array.
* If the key is found then try to match the corresponding
* regex against the User-Agent.
*
* @param string $key
*
* @return boolean
*/
protected function matchUAAgainstKey(string $key): bool
{
// Make the keys lowercase, so we can match: isIphone(), isiPhone(), isiphone(), etc.
$key = strtolower($key);
if (false === isset($this->cache[$key])) {
// change the keys to lower case
$_rules = array_change_key_case($this->getRules());
if (false === empty($_rules[$key])) {
$this->cache[$key] = $this->match($_rules[$key]);
}
if (false === isset($this->cache[$key])) {
$this->cache[$key] = false;
}
}
return $this->cache[$key];
}
/**
* Check if the device is mobile.
* Returns true if any type of mobile device detected, including special ones
* @param null $userAgent deprecated
* @param null $httpHeaders deprecated
* @return bool
*/
public function isMobile($userAgent = null, $httpHeaders = null): bool
{
if ($httpHeaders) {
$this->setHttpHeaders($httpHeaders);
}
if ($userAgent) {
$this->setUserAgent($userAgent);
}
// Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront'
if ($this->getUserAgent() === 'Amazon CloudFront') {
$cfHeaders = $this->getCfHeaders();
if (array_key_exists('HTTP_CLOUDFRONT_IS_MOBILE_VIEWER', $cfHeaders) &&
$cfHeaders['HTTP_CLOUDFRONT_IS_MOBILE_VIEWER'] === 'true'
) {
return true;
}
}
if ($this->checkHttpHeadersForMobile()) {
return true;
} else {
return $this->matchDetectionRulesAgainstUA();
}
}
/**
* Check if the device is a tablet.
* Return true if any type of tablet device is detected.
*
* @param string|null $userAgent deprecated
* @param array|null $httpHeaders deprecated
* @return bool
*/
public function isTablet(string $userAgent = null, array $httpHeaders = null): bool
{
// Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront'
if ($this->getUserAgent() === 'Amazon CloudFront') {
$cfHeaders = $this->getCfHeaders();
if (array_key_exists('HTTP_CLOUDFRONT_IS_TABLET_VIEWER', $cfHeaders) &&
$cfHeaders['HTTP_CLOUDFRONT_IS_TABLET_VIEWER'] === 'true'
) {
return true;
}
}
foreach (self::$tabletDevices as $_regex) {
if ($this->match($_regex, $userAgent)) {
return true;
}
}
return false;
}
/**
* This method checks for a certain property in the
* userAgent.
* @param string $key
* @param string|null $userAgent deprecated
* @param array|null $httpHeaders deprecated
* @return bool
*@todo: The httpHeaders part is not yet used.
*
*/
public function is(string $key, string $userAgent = null, array $httpHeaders = null): bool
{
// Set the UA and HTTP headers only if needed (eg. batch mode).
if ($httpHeaders) {
$this->setHttpHeaders($httpHeaders);
}
if ($userAgent) {
$this->setUserAgent($userAgent);
}
return $this->matchUAAgainstKey($key);
}
/**
* Some detection rules are relative (not standard),
* because of the diversity of devices, vendors and
* their conventions in representing the User-Agent or
* the HTTP headers.
*
* This method will be used to check custom regexes against
* the User-Agent string.
*
* @param $regex
* @param string|null $userAgent
* @return bool
*
* @todo: search in the HTTP headers too.
*/
public function match($regex, string $userAgent = null): bool
{
if (!\is_string($userAgent) && !\is_string($this->userAgent)) {
return false;
}
$match = (bool) preg_match(
sprintf('#%s#is', $regex),
(false === empty($userAgent) ? $userAgent : $this->userAgent),
$matches
);
// If positive match is found, store the results for debug.
if ($match) {
$this->matchingRegex = $regex;
$this->matchesArray = $matches;
}
return $match;
}
/**
* Get the properties array.
*
* @return array
*/
public static function getProperties(): array
{
return self::$properties;
}
/**
* Prepare the version number.
*
* @param string $ver The string version, like "2.6.21.2152";
*
* @return float
*@todo Remove the error suppression from str_replace() call.
*
*/
public function prepareVersionNo(string $ver): float
{
$ver = str_replace(array('_', ' ', '/'), '.', $ver);
$arrVer = explode('.', $ver, 2);
if (isset($arrVer[1])) {
$arrVer[1] = @str_replace('.', '', $arrVer[1]); // @todo: treat strings versions.
}
return (float) implode('.', $arrVer);
}
/**
* Check the version of the given property in the User-Agent.
* Will return a float number. (e.g. 2_0 will return 2.0, 4.3.1 will return 4.31)
*
* @param string $propertyName The name of the property. See self::getProperties() array
* keys for all possible properties.
* @param string $type Either self::VERSION_TYPE_STRING to get a string value or
* self::VERSION_TYPE_FLOAT indicating a float value. This parameter
* is optional and defaults to self::VERSION_TYPE_STRING. Passing an
* invalid parameter will default to the type as well.
*
* @return string|float|false The version of the property we are trying to extract.
*/
public function version(string $propertyName, string $type = self::VERSION_TYPE_STRING)
{
if (empty($propertyName)) {
return false;
}
if (!\is_string($this->userAgent)) {
return false;
}
// set the $type to the default if we don't recognize the type
if ($type !== self::VERSION_TYPE_STRING && $type !== self::VERSION_TYPE_FLOAT) {
$type = self::VERSION_TYPE_STRING;
}
$properties = self::getProperties();
// Check if the property exists in the properties array.
if (true === isset($properties[$propertyName])) {
// Prepare the pattern to be matched.
// Make sure we always deal with an array (string is converted).
$properties[$propertyName] = (array) $properties[$propertyName];
foreach ($properties[$propertyName] as $propertyMatchString) {
$propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString);
// Identify and extract the version.
preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match);
if (false === empty($match[1])) {
return ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]);
}
}
}
return false;
}
}
Rules/nrcoordinates.php 0000644 00000002254 15235314577 0011240 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Form\FormRule;
class JFormRuleNRCoordinates extends FormRule
{
/**
* The regular expression to use in testing a form field value.
*
* @var string
* @since 11.1
* @link http://www.w3.org/TR/html-markup/input.email.html
*/
protected $regex = '^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$';
public function test(SimpleXMLElement $element, $value, $group = null, Joomla\Registry\Registry $input = null, Joomla\CMS\Form\Form $form = null)
{
$value = trim($value);
// If the field is empty and not required, the field is valid.
$required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
if (!$required && empty($value))
{
return true;
}
// Test the value against the regular expression.
return parent::test($element, $value, $group, $input, $form);
}
} Rules/nrdate.php 0000644 00000001744 15235314577 0007646 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Form\FormRule;
class JFormRuleNRDate extends FormRule
{
public function test(SimpleXMLElement $element, $value, $group = null, Joomla\Registry\Registry $input = null, Joomla\CMS\Form\Form $form = null)
{
if (!$value = trim($value))
{
return true;
}
$format = (string) $element->attributes()->timeformat;
return $this->validateDate($value, $format);
}
/**
* Validates the given date with the given format
*
* @param string $date
* @param string $format
*
* @return boolean
*/
private function validateDate($date, $format = 'Y-m-d')
{
$d = DateTime::createFromFormat($format, $date);
return $d && $d->format($format) === $date;
}
} Integrations/HubSpot.php 0000644 00000005761 15235314577 0011334 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class HubSpot extends Integration
{
/**
* Create a new instance
*
* @param string $key Your HubSpot API key
*/
public function __construct($options)
{
parent::__construct();
$this->setKey(is_array($options) ? $options['api'] : $options);
$this->setEndpoint('https://api.hubapi.com');
}
/**
* Subscribe user to HubSpot
*
* API References:
* http://developers.hubspot.com/docs/methods/contacts/update_contact-by-email
*
* @param string $email User's email address
* @param string $params The forms extra fields
*
* @return void
*/
public function subscribe($email, $params)
{
$fields = $this->validateCustomFields($params);
$fields[] = array('property' => 'email', 'value' => $email);
$data = array(
'properties' => $fields
);
$this->post('contacts/v1/contact/createOrUpdate/email/' . $email . '/?hapikey=' . $this->key, $data);
return true;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* API References:
* http://developers.hubspot.com/docs/faq/api-error-responses
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if ((isset($body['status'])) && ($body['status'] == 'error'))
{
$message = $body['message'];
}
if (isset($body['validationResults']) && is_array($body['validationResults']) && count($body['validationResults']))
{
foreach ($body['validationResults'] as $key => $validation)
{
if ($validation['isValid'] === false)
{
$message .= ' - ' . $validation['message'];
}
}
}
return $message;
}
/**
* Returns a new array with valid only custom fields
*
* API References:
* http://developers.hubspot.com/docs/methods/contacts/v2/get_contacts_properties
*
* @param array $formCustomFields Array of custom fields
*
* @return array Array of valid only custom fields
*/
public function validateCustomFields($formCustomFields)
{
$fields = array();
if (!is_array($formCustomFields))
{
return $fields;
}
$accountFields = $this->get('properties/v1/contacts/properties?hapikey='.$this->key);
if (!$this->request_successful)
{
return $fields;
}
$accountFieldsNames = array_map(
function ($ar)
{
return $ar['name'];
}, $accountFields
);
$formCustomFieldsKeys = array_keys($formCustomFields);
foreach ($accountFieldsNames as $accountFieldsName)
{
if (!in_array($accountFieldsName, $formCustomFieldsKeys))
{
continue;
}
$fields[] = array(
"property" => $accountFieldsName,
"value" => $formCustomFields[$accountFieldsName],
);
}
return $fields;
}
} Integrations/ElasticEmail.php 0000644 00000010137 15235314577 0012275 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class ElasticEmail extends Integration
{
protected $endpoint = 'https://api.elasticemail.com/v2';
/**
* Create a new instance
*
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
}
/**
* Subscribe user to ElasticEmail
*
* API References:
* http://api.elasticemail.com/public/help#Contact_Add
* http://api.elasticemail.com/public/help#Contact_Update
*
* @param string $email User's email address
* @param string $list The ElasticEmail List unique ID
* @param string $publicAccountID The ElasticEmail PublicAccountID
* @param array $params The form's parameters
* @param boolean $update_existing Update existing user
* @param boolean $double_optin Send ElasticEmail confirmation email?
*
* @return void
*/
public function subscribe($email, $list, $publicAccountID, $params = array(), $update_existing = true, $double_optin = false)
{
$data = array(
'apikey' => $this->key,
'email' => $email,
'publicAccountID' => $publicAccountID,
'publicListID' => $list,
'sendActivation' => $double_optin ? 'true' : 'false',
'consentIP' => \NRFramework\User::getIP()
);
if (is_array($params) && count($params))
{
foreach ($params as $param_key => $param_value)
{
$data[$param_key] = (is_array($param_value)) ? implode(',', $param_value) : $param_value;
}
}
if (!$update_existing)
{
return $this->get('/contact/add', $data);
}
if ($this->getContact($email))
{
$data['clearRestOfFields'] = 'false';
$this->get('/contact/update', $data);
}
else
{
$this->get('/contact/add', $data);
}
return true;
}
/**
* Returns all available ElasticEmail lists
*
* http://api.elasticemail.com/public/help#List_list
*
* @return array
*/
public function getLists()
{
$data = $this->get('/list/list', array('apikey' => $this->key));
if (!$this->success())
{
return;
}
$lists = array();
if (!isset($data['data']) || !is_array($data['data']))
{
return $lists;
}
foreach ($data['data'] as $key => $list)
{
$lists[] = array(
'id' => $list['publiclistid'],
'name' => $list['listname']
);
}
return $lists;
}
/**
* Check to see if a contact exists
*
* @param string $email The contact's email
*
* @return boolean
*/
public function getContact($email)
{
$contact = $this->get('/contact/loadcontact', array('apikey' => $this->key, 'email' => $email));
return (bool) $contact['success'];
}
/**
* Get the Elastic Email Public Account ID
*
* @return string
*/
public function getPublicAccountID()
{
$data = $this->get('/account/load', array('apikey' => $this->key));
if (isset($data['data']['publicaccountid']))
{
return $data['data']['publicaccountid'];
}
throw new \Exception(Text::_('NR_ELASTICEMAIL_UNRETRIEVABLE_PUBLICACCOUNTID'), 1);
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
if (isset($body['error']))
{
return $body['error'];
}
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$code = $this->last_response->code;
$body = $this->last_response->body;
if ($code >= 200 && $code <= 299 && !isset($body['error']))
{
return ($this->request_successful = true);
}
$this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
return false;
}
} Integrations/MailChimp.php 0000644 00000030662 15235314577 0011611 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
use NRFramework\Functions;
// No direct access
defined('_JEXEC') or die;
class MailChimp extends Integration
{
/**
* MailChimp Endpoint URL
*
* @var string
*/
protected $endpoint = 'https://<dc>.api.mailchimp.com/3.0';
/**
* Create a new instance
*
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
if (strpos($this->key, '-') === false)
{
return;
}
list(, $data_center) = explode('-', $this->key);
$this->endpoint = str_replace('<dc>', $data_center, $this->endpoint);
$this->options->set('headers.Authorization', 'apikey ' . $this->key);
}
/**
* Subscribe user to MailChimp
*
* @param string $list_id The ID of the MailChimp list
* @param string $email The email address of the subscriber
* @param object $merge_fields The custom field that are associated with the subscriber where the keys are the merge tags.
* @param boolean $double_optin If true, the subscriber will be added with status "pending" and a confirmation email will be sent to the user.
* @param boolean $allow_update If true, the subscriber will be updated if it already exists. Otherwise, an error will be thrown.
* @param array $tags The tags that are associated with the subscriber.
* @param string $tags_replace Determines what changes to make to the subscriber's tags. Values: add_only, replace_all
* @param array $interests The interests that are associated with the subscriber.
* @param string $interests_replace Determines what changes to make to the subscriber's groups/interests. Values: add_only, replace_all
*
* @return void
*/
public function subscribeV2($list_id, $email, $merge_fields = null, $double_optin = true, $allow_update = true, $tags = null, $tags_replace = 'add_only', $interests = [], $interests_replace = 'add_only')
{
$data = [
'email_address' => $email,
'status' => $double_optin ? 'pending' : 'subscribed',
'merge_fields' => (object) $merge_fields,
'tags' => Functions::cleanArray($tags)
];
$member = $this->getMemberByEmail($list_id, $email);
// Prepare Interests
$interests = Functions::cleanArray($interests);
$interests = $interests ? array_fill_keys($interests, true) : [];
if ($member && isset($member['interests']) && $interests_replace == 'replace_all')
{
// Disable all existing groups
$memberInterests = array_fill_keys(array_keys($member['interests']), false);
// Merge new interests with existing interests
$interests = array_merge($memberInterests, $interests);
}
$data['interests'] = (object) $interests;
if (!$member)
{
// API Doc: https://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/#create-post_lists_list_id_members
$this->post('lists/' . $list_id . '/members', $data);
return;
}
// Member exists
// Since member exists and we don't allow updating existing member, throw an error.
if (!$allow_update)
{
throw new \Exception('Member already exists');
}
// Skip double opt-in if the existing member is already confirmed
if (isset($member['status']) && $member['status'] == 'subscribed')
{
$data['status'] = $member['status'];
}
// Update existing member
// API Doc: https://mailchimp.com/developer/marketing/api/list-members/add-or-update-list-member
$this->put('lists/' . $list_id . '/members/' . $member['id'], $data);
// Remove existing member tags not included in the given Tags.
if ($member['tags'] && $tags_replace == 'replace_all')
{
$currentTags = array_map(function($item) { return $item['name']; }, $member['tags']);
if ($removeTags = array_diff($currentTags, $data['tags']))
{
$rTags = [];
foreach ($removeTags as $removeTag)
{
$rTags[] = [
'name' => $removeTag,
'status' => 'inactive'
];
}
// API Doc: https://mailchimp.com/developer/marketing/api/list-member-tags/add-or-remove-member-tags/
$this->post('lists/' . $list_id . '/members/' . $member['id'] . '/tags', ['tags' => $rTags]);
}
}
}
/**
* Subscribe user to MailChimp
*
* API References:
* https://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/#edit-put_lists_list_id_members_subscriber_hash
* https://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/#create-post_lists_list_id_members
*
* @param string $email User's email address
* @param string $list The MailChimp list unique ID
* @param Object $merge_fields Merge Fields
* @param boolean $update_existing Update existing user
* @param boolean $double_optin Send MailChimp confirmation email?
*
* @deprecated Use subscribeV2()
*
* @return void
*/
public function subscribe($email, $list, $merge_fields = array(), $update_existing = true, $double_optin = false)
{
$data = array(
'email_address' => $email,
'status' => $double_optin ? 'pending' : 'subscribed'
);
// add support for tags
if ($tags = $this->getTags($merge_fields))
{
$data['tags'] = $tags;
}
if (is_array($merge_fields) && count($merge_fields))
{
foreach ($merge_fields as $merge_field_key => $merge_field_value)
{
$value = is_array($merge_field_value) ? implode(',', $merge_field_value) : (string) $merge_field_value;
$data['merge_fields'][$merge_field_key] = $value;
}
}
$interests = $this->validateInterestCategories($list, $merge_fields);
if (!empty($interests))
{
$data = array_merge($data, array('interests' => $interests));
}
if ($update_existing)
{
// Get subscriber information.
$subscriberHash = md5(strtolower($email));
$member = $this->get('lists/' . $list . '/members/' . $subscriberHash);
// Skip double opt-in if the subscriber exists and it's confirmed
if (isset($member['status']) && $member['status'] == 'subscribed')
{
$data['status'] = $member['status'];
}
$this->put('lists/' . $list . '/members/' . $subscriberHash, $data);
if ($tags)
{
$tags_ = [];
foreach ($tags as $tag)
{
$tags_[] = [
'name' => $tag,
'status' => 'active'
];
}
$currentTags = $this->getMemberTags($list, $subscriberHash);
if ($removeTags = array_diff($currentTags, $tags))
{
foreach ($removeTags as $removeTag)
{
$tags_[] = [
'name' => $removeTag,
'status' => 'inactive'
];
}
}
$this->post('lists/' . $list . '/members/' . $subscriberHash . '/tags', ['tags' => $tags_]);
}
} else
{
$this->post('lists/' . $list . '/members', $data);
}
return true;
}
/**
* @deprecated Use subscribeV2()
*/
private function getMemberTags($list, $subscriberHash)
{
$tags = $this->get('lists/' . $list . '/members/' . $subscriberHash . '/tags');
$return = [];
if (isset($tags['tags']))
{
foreach ($tags['tags'] as $tag)
{
$return[] = $tag['name'];
}
}
return $return;
}
/**
* Find and return all unique tags
*
* @param array $merge_fields
*
* @deprecated use subscribeV2()
*
* @return array
*/
private function getTags($merge_fields)
{
$tags = [];
// ensure tags are added in the form
if (!isset($merge_fields['tags']))
{
return $tags;
}
$mergeFieldsTags = $merge_fields['tags'];
// make string array
if (is_string($mergeFieldsTags))
{
$tags = explode(',', $mergeFieldsTags);
}
// ensure we have array to manipulate
if (is_array($mergeFieldsTags) || is_object($mergeFieldsTags))
{
$tags = (array) $mergeFieldsTags;
}
// remove empty values, keep uniques and reset keys
$tags = array_filter($tags);
$tags = array_unique($tags);
$tags = array_values($tags);
$tags = array_map('trim', $tags);
return $tags;
}
/**
* Returns all available MailChimp lists
*
* https://developer.mailchimp.com/documentation/mailchimp/reference/lists/#read-get_lists
*
* @return array
*/
public function getLists()
{
$data = $this->get('/lists');
if (!$this->success())
{
return;
}
if (!isset($data['lists']) || !is_array($data['lists']))
{
return;
}
$lists = [];
foreach ($data['lists'] as $key => $list)
{
$lists[] = array(
'id' => $list['id'],
'name' => $list['name']
);
}
return $lists;
}
/**
* Gets the Interest Categories from MailChimp
*
* @param string $listID The List ID
*
* @deprecated Use subscribeV2()
*
* @return array
*/
public function getInterestCategories($listID)
{
if (!$listID)
{
return;
}
$data = $this->get('/lists/' . $listID . '/interest-categories');
if (!$this->success())
{
return;
}
if (isset($data['total_items']) && $data['total_items'] == 0)
{
return;
}
return $data['categories'];
}
/**
* Gets the values accepted for the particular Interest Category
*
* @param string $listID The List ID
* @param string $interestCategoryID The Interest Category ID
*
* @deprecated Use subscribeV2()
*
* @return array
*/
public function getInterestCategoryValues($listID, $interestCategoryID)
{
if (!$interestCategoryID || !$listID)
{
return array();
}
$data = $this->get('/lists/' . $listID . '/interest-categories/' . $interestCategoryID . '/interests');
if (isset($data['total_items']) && $data['total_items'] == 0)
{
return array();
}
return $data['interests'];
}
/**
* Filters the interests categories through the form fields
* and constructs the interests array for the subscribe method
*
* @param string $listID The List ID
* @param array $params The Form fields
*
* @deprecated Use subscribeV2()
*
* @return array
*/
public function validateInterestCategories($listID, $params)
{
if (!$params || !$listID)
{
return array();
}
$interestCategories = $this->getInterestCategories($listID);
if (!$interestCategories)
{
return array();
}
$categories = array();
foreach ($interestCategories as $category)
{
if (array_key_exists($category['title'], $params))
{
$categories[] = array('id' => $category['id'], 'title' => $category['title']);
}
}
if (empty($categories))
{
return array();
}
$interests = array();
foreach ($categories as $category)
{
$data = $this->getInterestCategoryValues($listID, $category['id']);
if (isset($data['total_items']) && $data['total_items'] == 0)
{
continue;
}
foreach ($data as $interest)
{
if (in_array($interest['name'], (array) $params[$category['title']]))
{
$interests[$interest['id']] = true;
}
else
{
$interests[$interest['id']] = false;
}
}
}
return $interests;
}
/**
* Find a subscriber in a list by email address
*
* @param string $list_id The MailChimp list ID
* @param string $email The email address
*
* @return mixed Object on success, false on failure
*/
public function getMemberByEmail($list_id, $email)
{
$subscriberHash = md5(strtolower($email));
$result = $this->get('lists/' . $list_id . '/members/' . $subscriberHash);
return $this->success() ? $result : false;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
if (isset($body['errors']))
{
$error = $body['errors'][0];
return $error['field'] . ': ' . $error['message'];
}
if (isset($body['detail']))
{
return $body['detail'];
}
}
/**
* The get() method overridden so that it handles
* the default item paging of MailChimp which is 10
*
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function get($method, $args = array())
{
$data = $this->makeRequest('get', $method, $args);
if ($data && isset($data['total_items']) && (int) $data['total_items'] > 10)
{
$args['count'] = $data['total_items'];
return $this->makeRequest('get', $method, $args);
}
return $data;
}
} Integrations/ZohoCRM.php 0000644 00000007614 15235314577 0011230 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class ZohoCRM extends Integration
{
/**
* Response Type
*
* @var string
*/
protected $response_type = 'xml';
/**
* Data Center API Endpoint
*
* @var string
*/
private $datacenter = 'crm.zoho.com';
/**
* Create a new instance
*
* @param array $options The service's required options
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options['authenticationToken']);
if (isset($options['datacenter']) && !is_null($options['datacenter']) && !empty($options['datacenter']))
{
$this->datacenter = $options['datacenter'];
}
}
/**
* Subscribe user to ZohoCRM
*
* https://www.zoho.eu/crm/help/api/insertrecords.html#Insert_records_into_Zoho_CRM_from_third-party_applications
*
* @param string $email User's email address
* @param array $fields Available form fields
* @param string $module Zoho module to be used
* @param boolean $update_existing Update existing users
* @param string $workflow Trigger the workflow rule while inserting record
* @param string $approve Approve records (Supports: Leads, Contacts, and Cases modules)
*
* @return void
*/
public function subscribe($email, $fields, $module = 'leads', $update_existing = true, $workflow = false, $approve = false)
{
$data = array(
'authtoken' => $this->key,
'scope' => 'crmapi',
'xmlData' => $this->buildModuleXML($email, $fields, $module),
'duplicateCheck' => $update_existing ? '2' : '1',
'wfTrigger' => $workflow ? 'true' : 'false',
'isApproval' => $approve ? 'true' : 'false',
'version' => '4'
);
$this->endpoint = 'https://' . $this->datacenter . '/crm/private/xml/' . ucfirst($module) . '/insertRecords?' . http_build_query($data);
$this->post('');
}
/**
* Build the XML for each module
*
* @param string $email User's email address
* @param array $fields Form fields
* @param string $module Module to be used
*
* @return string The XML
*/
private function buildModuleXML($email, $fields, $module)
{
$xml = new SimpleXMLElement('<' . ucfirst($module) . '/>');
$row = $xml->addChild('row');
$row->addAttribute('no', '1');
$xmlField = $row->addChild('FL', $email);
$xmlField->addAttribute('val', 'Email');
if (is_array($fields) && count($fields))
{
foreach ($fields as $field_key => $field_value)
{
$field_value = is_array($field_value) ? implode(',', $field_value) : $field_value;
$xmlField = $row->addChild('FL', $field_value);
$xmlField->addAttribute('val', $field_key);
}
}
return $xml->asXML();
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
if (isset($body->error))
{
return $body->error->message;
}
if (isset($body->result->row->error))
{
return $body->result->row->error->details;
}
return 'Unknown error';
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
public function determineSuccess()
{
$status = $this->last_response->code;
$success = ($status >= 200 && $status <= 299) ? true : false;
if (!$success)
{
return false;
}
$body = $this->last_response->body;
if (!isset($body->result->row->success))
{
return false;
}
return ($this->request_successful = true);
}
} Integrations/Drip.php 0000644 00000016045 15235314577 0010643 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class Drip extends Integration
{
/**
* Create a new instance
*
* @param string $key Your Drip API key
* @param string $account_id Your Drip Account ID
*/
public function __construct($options)
{
parent::__construct();
if (!(isset($options['api']) && isset($options['account_id']))) {
return;
}
$this->setKey($options['api']);
$this->setEndpoint('https://api.getdrip.com/v2/' . $options['account_id']);
$this->options->set('headers.Authorization', 'Basic ' . base64_encode($this->key . ':'));
}
/**
* Subscribe user to Drip
*
* API References:
* https://developer.drip.com/#create-or-update-a-subscriber
*
* @param string $email User's email address
* @param string $campaign_id The Campaign ID
* @param string $name The name of the Contact (Name can be also declared in Custom Fields)
* @param Object $custom_fields Custom Fields
* @param mixed $tags Tags for this contact (comma-separated). Example: 'tag1, tag2, etc'
* @param boolean $update_existing Update existing user
* @param boolean $double_optin Send MailChimp confirmation email?
*
* @return void
*/
public function subscribe($email, $campaign_id, $name = null, $custom_fields = array(), $tags = '', $update_existing = true, $double_optin = false)
{
// Detect name
$name = (is_null($name) || empty($name)) ? $this->getNameFromCustomFields($custom_fields) : explode(' ', $name, 2);
// We use this boolean to see if the user has subscribed the campaign
// This is used for the `update_existing` parameter
$subscriber_exists = $this->subscriberIsInCampaign($email, $campaign_id);
// Check if we need to update the user
if ($update_existing == false && $subscriber_exists)
{
throw new \Exception(Text::_('NR_DRIP_SUBSCRIBER_ALREADY_EXISTS'), 1);
}
// Remove tags from custom fields
$custom_fields_parse = $custom_fields;
if (isset($custom_fields_parse['tags']))
{
unset($custom_fields_parse['tags']);
}
// Create or Update a Subscriber
$data = [
'subscribers' => [
[
'email' => $email,
'first_name' => isset($name[0]) ? $name[0] : '',
'last_name' => isset($name[1]) ? $name[1] : '',
'address1' => $this->getCustomFieldValue('address1', $custom_fields),
'address2' => $this->getCustomFieldValue('address2', $custom_fields),
'city' => $this->getCustomFieldValue('city', $custom_fields),
'state' => $this->getCustomFieldValue('state', $custom_fields),
'zip' => $this->getCustomFieldValue('zip', $custom_fields),
'country' => $this->getCustomFieldValue('country', $custom_fields),
'phone' => $this->getCustomFieldValue('phone', $custom_fields),
'custom_fields' => $custom_fields_parse,
'tags' => $this->getTags($tags)
]
]
];
$this->post('subscribers', $data);
// If we are updating a user, dont try re-assigning him to a campaign
// If we are updating a user but he just subscribed, then assign him to a campaign
if ($update_existing == false || $subscriber_exists == false)
{
// Assign the newly created subscriber to the campaign
$this->assignSubscriberToCampaign($email, $campaign_id, $double_optin);
}
return true;
}
/**
* Assign a Subscriber to a Campaign
*
* https://developer.drip.com/?shell#subscribe-someone-to-a-campaign
*
* @return void
*/
private function assignSubscriberToCampaign($email, $campaign_id, $double_optin)
{
// Subscribe user to a campaign
$campaignSubAPI = 'campaigns/' . $campaign_id . '/subscribers';
$data = [
'subscribers' => [
[
'email' => $email,
'double_optin' => (bool) $double_optin
]
]
];
$this->post($campaignSubAPI, $data);
}
/**
* Returns an array of tags or an empty string if no tags provided
*
* @return mixed
*/
private function getTags($tags) {
if (empty($tags))
{
return;
}
if (is_string($tags))
{
$tags = array_map('trim', explode(',', $tags));
}
return $tags;
}
/**
* Returns whether the subscriber is in a campaign
*
* https://developer.drip.com/?shell#list-all-of-a-subscriber-39-s-campaign-subscriptions
*
* @return bool
*/
private function subscriberIsInCampaign($email, $campaign_id)
{
$found_campaign = false;
$subscriber_id = $this->getSubscriberIdFromEmail($email);
// Use does not exist in Drip
if (empty($subscriber_id))
{
return false;
}
$subscriber_campaigns = $this->getSubscriberCampaigns($subscriber_id);
foreach ($subscriber_campaigns as $c)
{
if ($c['campaign_id'] == $campaign_id)
{
$found_campaign = true;
break;
}
}
return $found_campaign;
}
/**
* Returns the ID of the subscriber from email
*
* https://developer.drip.com/?shell#fetch-a-subscriber
*
* @return string
*/
private function getSubscriberIdFromEmail($email)
{
$data = $this->get('subscribers/' . $email);
return isset($data['subscribers']) ? $data['subscribers'][0]['id'] : '';
}
/**
* Returns all subscriber's campaigns
*
* https://developer.drip.com/?javascript#list-all-of-a-subscriber-39-s-campaign-subscriptions
*
* @return array
*/
private function getSubscriberCampaigns($subscriberId)
{
$data = $this->get('subscribers/' . $subscriberId . '/campaign_subscriptions');
return isset($data['campaign_subscriptions']) ? $data['campaign_subscriptions'] : array();
}
/**
* Returns all available Drip campaigns
*
* https://developer.drip.com/?shell#list-all-campaigns
*
* @return array
*/
public function getLists()
{
$data = $this->get('campaigns');
if (!$this->success())
{
return;
}
if (!isset($data['campaigns']) || !is_array($data['campaigns']))
{
return;
}
$campaigns = [];
foreach ($data['campaigns'] as $key => $campaign)
{
$campaigns[] = array(
'id' => $campaign['id'],
'name' => $campaign['name']
);
}
return $campaigns;
}
/**
* Search for First Name and Last Name in Custom Fields and return an array with both values.
*
* @param array $custom_fields The Custom Fields array passed by the user.
*
* @return array
*/
private function getNameFromCustomFields($custom_fields)
{
return [
(string) $this->getCustomFieldValue(['first_name', 'First Name'], $custom_fields),
(string) $this->getCustomFieldValue(['last_name', 'Last Name'], $custom_fields)
];
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$messages = '';
if (isset($body['errors']))
{
foreach ($body['errors'] as $error)
{
$messages .= ' - ' . $error['message'];
}
}
return $messages;
}
} Integrations/Turnstile.php 0000644 00000004361 15235314577 0011734 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class Turnstile extends Integration
{
/**
* Service Endpoint
*
* @var string
*/
protected $endpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
/**
* Create a new instance
*
* @param array $options
*
* @throws \Exception
*/
public function __construct($options = [])
{
parent::__construct();
if (!array_key_exists('secret', $options))
{
$this->setError('NR_RECAPTCHA_INVALID_SECRET_KEY');
throw new \Exception($this->getLastError());
}
$this->setKey($options['secret']);
}
/**
* Calls the Cloudflare Turnstile siteverify API to verify whether the user passes the test.
*
* @param string $response Response string from Cloudflare Turnstile verification.
* @param string $remoteip IP address of end user
*
* @return bool Returns true if the user passes the test
*/
public function validate($response, $remoteip = null)
{
if (empty($response) || is_null($response))
{
return $this->setError('NR_RECAPTCHA_PLEASE_VALIDATE');
}
$data = [
'secret' => $this->key,
'response' => $response,
];
$this->post('', $data);
return true;
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$success = parent::determineSuccess();
$body = $this->last_response->body;
if ($body['success'] == false && array_key_exists('error-codes', $body) && count($body['error-codes']) > 0)
{
$success = $this->setError(implode(', ', $body['error-codes']));
}
return ($this->request_successful = $success);
}
/**
* Set wrapper error text
*
* @param String $error The error message to display
*/
private function setError($error)
{
$this->last_error = Text::_('NR_TURNSTILE') . ': ' . Text::_($error);
return false;
}
} Integrations/SendInBlue3.php 0000644 00000004573 15235314577 0012023 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class SendInBlue3 extends Integration
{
/**
* Create a new instance
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options['api']);
$this->setEndpoint('https://api.sendinblue.com/v3');
$this->options->set('headers.api-key', $this->key);
}
/**
* Subscribes a user to a SendinBlue Account
*
* API Reference v3:
* https://developers.sendinblue.com/reference#createcontact
*
* @param string $email The user's email
* @param array $params All the form fields
* @param string $listid The List ID
* @param boolean $update_existing Whether to update the existing contact (Only in v3)
*
* @return boolean
*/
public function subscribe($email, $params, $listid = false, $update_existing = true)
{
$data = [
'email' => $email,
'attributes' => (object) $params,
'updateEnabled' => $update_existing
];
if ($listid)
{
$data['listIds'] = [(int) $listid];
}
$this->post('contacts', $data);
return true;
}
/**
* Returns all Campaign lists
*
* API Reference v3:
* https://developers.sendinblue.com/reference#getlists-1
*
* @return array
*/
public function getLists()
{
$data = [
'page' => 1,
'page_limit' => 50
];
$lists = [];
$data = $this->get('contacts/lists', $data);
// sanity check
if (!isset($data['lists']) || !is_array($data['lists']) || $data['count'] == 0)
{
return $lists;
}
foreach ($data['lists'] as $key => $list)
{
$lists[] = [
'id' => $list['id'],
'name' => $list['name']
];
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* API Reference:
* https://developers.sendinblue.com/docs/how-it-works#error-codes
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (!isset($body['code']))
{
return $message;
}
return $body['message'];
}
} Integrations/HubSpot3.php 0000644 00000010434 15235314577 0011410 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class HubSpot3 extends Integration
{
/**
* Create a new instance
*
* @param string $key Your HubSpot API key
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
$this->setEndpoint('https://api.hubapi.com/crm/v3');
$this->options->set('headers.Authorization', 'Bearer ' . $this->key);
}
/**
* Create/Update a HubSpot Contact
*
* API References:
* https://developers.hubspot.com/docs/api/crm/contacts
*
* @param string $email User's email address
* @param string $params The forms extra fields
* @param bool $update_existing Set whether to update an existing user
*
* @return void
*/
public function subscribe($email, $params, $update_existing = true)
{
$contact_data = $this->contactExists($email);
if (!$update_existing)
{
if ($contact_data)
{
throw new \Exception('Contact already exists.');
}
}
$default_property = ['email' => $email];
$other_properties = $this->validateCustomFields($params);
$data = [
'properties' => array_merge($default_property, $other_properties)
];
$method = 'post';
$endpoint = 'objects/contacts';
if ($update_existing && $contact_data)
{
$method = 'patch';
$endpoint .= '/' . $contact_data['id'];
};
$this->$method($endpoint, $data);
// If a list exists, add the contact to that list.
if ($this->success() && isset($params['list']) && !empty($params['list']))
{
$this->addContactToStaticList($email, $params['list']);
}
}
/**
* Returns all lists.
*
* @return array
*/
public function getLists()
{
$this->endpoint = $this->getV1Endpoint();
$data = $this->get('lists/static');
if (!$this->success())
{
return;
}
if (!is_array($data) || !count($data) || !isset($data['lists']))
{
return;
}
$lists = [];
foreach ($data['lists'] as $key => $list)
{
$lists[] = [
'id' => $list['listId'],
'name' => $list['name']
];
}
return $lists;
}
/**
* Add contact to a static list.
*
* @param string $email
* @param int $list_id
*
* @return void
*/
public function addContactToStaticList($email, $list_id)
{
$this->endpoint = $this->getV1Endpoint();
$data = (object) [ 'emails' => [ $email ] ];
$this->post('lists/' . $list_id . '/add', $data);
}
/**
* Return the v1 endpoint.
*
* @return string
*/
private function getV1Endpoint()
{
return 'https://api.hubapi.com/contacts/v1';
}
/**
* Check whether contact already exists.
*
* @param string $email
*
* @return bool
*/
public function contactExists($email)
{
$contact = $this->get('objects/contacts/' . $email . '?idProperty=email');
return $this->success() ? $contact : false;
}
/**
* Returns a new array with valid only custom fields
*
* API References:
* https://developers.hubspot.com/docs/api/crm/properties
*
* @param array $formCustomFields Array of custom fields
*
* @return array Array of valid only custom fields
*/
public function validateCustomFields($formCustomFields)
{
$fields = [];
if (!is_array($formCustomFields))
{
return $fields;
}
$contactCustomFields = $this->get('properties/Contact');
if (!$this->request_successful)
{
return $fields;
}
$customFieldNames = array_map(
function ($ar)
{
return $ar['name'];
}, $contactCustomFields['results']
);
$formCustomFieldsKeys = array_keys($formCustomFields);
foreach ($customFieldNames as $accountFieldName)
{
if (!in_array($accountFieldName, $formCustomFieldsKeys))
{
continue;
}
$fields[$accountFieldName] = $formCustomFields[$accountFieldName];
}
return $fields;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
if (isset($body['status']) && $body['status'] === 'error')
{
return $body['message'];
}
}
} Integrations/IContact.php 0000644 00000010671 15235314577 0011450 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class IContact extends Integration
{
public $accountID;
public $clientFolderID;
/**
* Create a new instance
* @param array $options The service's required options
*/
public function __construct($options)
{
parent::__construct();
$this->endpoint = 'https://app.icontact.com/icp/a';
$this->options->set('headers.API-Version', '2.2');
$this->options->set('headers.API-AppId', $options['appID']);
$this->options->set('headers.API-Username', $options['username']);
$this->options->set('headers.API-Password', $options['appPassword']);
$this->setAccountID($options['accountID']);
$this->setClientFolderID($options['clientFolderID']);
}
/**
* Finds and sets the iContact AccountID
*
* @param mixed $accountID
*/
public function setAccountID($accountID = false)
{
if ($accountID)
{
$this->accountID = $accountID;
}
$accounts = $this->get('');
if (!$this->success())
{
throw new \Exception($this->getLastError());
}
// Make sure the account is active
if (intval($accounts['accounts'][0]['enabled']) === 1)
{
$this->accountID = (integer) $accounts['accounts'][0]['accountId'];
}
else
{
throw new \Exception(Text::_('NR_ICONTACT_ACCOUNTID_ERROR'), 1);
}
}
/**
* Finds and sets the iContact ClientFolderID
*
* @param mixed $clientFolderID
*/
public function setClientFolderID($clientFolderID = false)
{
if ($clientFolderID)
{
$this->clientFolderID = $clientFolderID;
}
// We need an existant accountID
if (empty($this->accountID))
{
try
{
$this->setAccountID();
}
catch (Exception $e)
{
throw $e;
}
}
if ($clientFolder = $this->get($this->accountID . '/c/'))
{
$this->clientFolderID = $clientFolder['clientfolders'][0]['clientFolderId'];
}
}
/**
* Subscribes a user to an iContact List
*
* API REFERENCE
* https://www.icontact.com/developerportal/documentation/contacts
*
* @param string $email
* @param object $params The extra form fields
* @param mixed $list The iContact List ID
*
* @return boolean
*/
public function subscribe($email, $params, $list)
{
$data = array('contact' => array_merge(array('email' => $email, 'status' => 'normal'), (array) $params));
try
{
$contact = $this->post($this->accountID .'/c/' . $this->clientFolderID . '/contacts', $data);
}
catch (Exception $e)
{
throw $e;
}
if ((isset($contact['contacts'])) && (is_array($contact['contacts'])) && (count($contact['contacts']) > 0))
{
$this->addToList($list, $contact['contacts'][0]['contactId']);
}
return true;
}
/**
* Adds a contact to an iContact List
*
* API REFERENCE
* https://www.icontact.com/developerportal/documentation/subscriptions
*
* @param string $listID
* @param string $contactID
*/
public function addToList($listID, $contactID)
{
$data = array(
array(
'contactId' => $contactID,
'listId' => $listID,
'status' => 'normal'
)
);
$this->post($this->accountID .'/c/' . $this->clientFolderID . '/subscriptions',$data);
}
/**
* Returns all Client lists
*
* API REFERENCE
* https://www.icontact.com/developerportal/documentation/lists
*
* @return array
*/
public function getLists()
{
$data = $this->get($this->accountID .'/c/' . $this->clientFolderID . '/lists');
if (!$this->success())
{
return;
}
$lists = array();
if (!isset($data["lists"]) || !is_array($data["lists"]))
{
return $lists;
}
foreach ($data["lists"] as $key => $list)
{
$lists[] = array(
'id' => $list['listId'],
'name' => $list['name']
);
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
* If something didn't work, this should contain the string describing the problem.
*
* @return string describing the error
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (isset($body['errors']))
{
foreach ($body['errors'] as $error) {
$message .= $error . ' ';
}
}
return trim($message);
}
} Integrations/ConvertKit.php 0000644 00000006763 15235314577 0012043 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\String\StringHelper;
class ConvertKit extends Integration
{
/**
* Create a new instance
*
* @param string $api_key Your ConvertKit API Key
*/
public function __construct($api_key)
{
parent::__construct();
$this->setKey($api_key);
$this->setEndpoint('https://api.convertkit.com/v3');
}
/**
* Subscribe a user to a ConvertKit Form
*
* API Reference:
* http://help.convertkit.com/article/33-api-documentation-v3
*
* @param string $email The subscriber's email
* @param string $formid The account owner's form id
* @param array $params The form's parameters
*
* @return boolean
*/
public function subscribe($email, $formid, $params)
{
$first_name = (isset($params['first_name'])) ? $params['first_name'] : '';
$tags = (isset($params['tags'])) ? $this->convertTagnamesToTagIDs($params['tags']) : '';
$fields = $this->validateCustomFields($params);
$data = array(
'api_key' => $this->key,
'email' => $email,
'first_name' => $first_name,
'tags' => $tags,
'fields' => $fields,
);
$this->post('forms/' . $formid . '/subscribe', $data);
return true;
}
/**
* Converts tag names to tag IDs for the subscribe method
*
* @param string $tagnames comma separated list of tagnames
*
* @return string comma separated list of tag IDs
*/
public function convertTagnamesToTagIDs($tagnames)
{
if (empty($tagnames))
{
return;
}
$tagArray = !is_array($tagnames) ? explode(',', $tagnames) : $tagnames;
$tagnames = array_map('trim', $tagArray);
$accountTags = $this->get('tags', array('api_key' => $this->key));
if (empty($accountTags) || !$this->request_successful)
{
return;
}
$tagIDs = array();
foreach ($accountTags['tags'] as $tag)
{
foreach ($tagnames as $tagname)
{
if (StringHelper::strcasecmp($tag['name'], $tagname) == 0)
{
$tagIDs[] = $tag['id'];
break;
}
}
}
return implode(',', $tagIDs);
}
/**
* Returns a new array with valid only custom fields
*
* @param array $formCustomFields Array of custom fields
*
* @return array Array of valid only custom fields
*/
public function validateCustomFields($formCustomFields)
{
if (!is_array($formCustomFields))
{
return;
}
$customFields = $this->get('custom_fields', array('api_key' => $this->key));
if (!$this->request_successful)
{
return;
}
$fields = array();
$formCustomFieldsKeys = array_keys($formCustomFields);
foreach ($customFields['custom_fields'] as $customField)
{
if (in_array($customField['key'], $formCustomFieldsKeys))
{
$fields[$customField['key']] = $formCustomFields[$customField['key']];
}
}
return $fields;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (isset($body['error']) && !empty($body['error']))
{
$message = $body['error'];
}
if (isset($body['message']) && !empty($body['message']))
{
$message .= ' - ' . $body['message'];
}
return $message;
}
} Integrations/Salesforce.php 0000644 00000004134 15235314577 0012027 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
class SalesForce extends Integration
{
/**
* Service API Endpoint
*
* @var string
*/
protected $endpoint = 'https://webto.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8';
/**
* Encode data before sending the request
*
* @var boolean
*/
protected $encode = false;
/**
* Create a new instance
* @param string $organizationID Your SalesForce Organization ID
* @throws \Exception
*/
public function __construct($organization_id)
{
parent::__construct();
$organization_id = is_array($organization_id) ? $organization_id['api'] : $organization_id;
$this->setKey($organization_id);
$this->options->set('headers.Content-Type', 'application/x-www-form-urlencoded');
}
/**
* Subscribe user to SalesForce
*
* API References:
* https://developer.salesforce.com/page/Wordpress-to-lead
*
* @param string $email User's email address
* @param array $params All the form fields
*
* @return void
*/
public function subscribe($email, $params)
{
$data = array(
"email" => $email,
"oid" => $this->key
);
if (is_array($params) && count($params))
{
$data = array_merge($data, $params);
}
$this->post('', $data);
return true;
}
/**
* Determine if the Lead has been stored successfully in SalesForce
*
* @return string
*/
public function determineSuccess()
{
$status = $this->last_response->code;
if ($status < 200 && $status > 299)
{
return false;
}
$headers = $this->last_response->headers;
if (isset($headers['Is-Processed']) && (strpos($headers['Is-Processed'], 'Exception') !== false))
{
$this->last_error = Text::_('NR_SALESFORCE_ERROR');
return false;
}
return ($this->request_successful = true);
}
} Integrations/ReCaptcha.php 0000644 00000004500 15235314577 0011570 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/**
* The reCAPTCHA Wrapper
*/
class ReCaptcha extends Integration
{
/**
* Service Endpoint
*
* @var string
*/
protected $endpoint = 'https://www.google.com/recaptcha/api/siteverify';
/**
* Create a new instance
*
* @param array $options
*
* @throws \Exception
*/
public function __construct($options = array())
{
parent::__construct();
if (!array_key_exists('secret', $options))
{
$this->setError('NR_RECAPTCHA_INVALID_SECRET_KEY');
throw new \Exception($this->getLastError());
}
$this->setKey($options['secret']);
}
/**
* Calls the reCAPTCHA siteverify API to verify whether the user passes reCAPTCHA test.
*
* @param string $response Response string from recaptcha verification.
* @param string $remoteip IP address of end user
*
* @return bool Returns true if the user passes reCAPTCHA test
*/
public function validate($response, $remoteip = null)
{
if (empty($response) || is_null($response))
{
return $this->setError('NR_RECAPTCHA_PLEASE_VALIDATE');
}
$data = array(
'secret' => $this->key,
'response' => $response,
'remoteip' => $remoteip ?: \NRFramework\User::getIP(),
);
$this->get('', $data);
return true;
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$success = parent::determineSuccess();
$body = $this->last_response->body;
if ($body['success'] == false && array_key_exists('error-codes', $body) && count($body['error-codes']) > 0)
{
$success = $this->setError(implode(', ', $body['error-codes']));
}
return ($this->request_successful = $success);
}
/**
* Set wrapper error text
*
* @param String $error The error message to display
*/
private function setError($error)
{
$this->last_error = Text::_('NR_RECAPTCHA') . ': ' . Text::_($error);
return false;
}
} Integrations/GetResponse.php 0000644 00000015007 15235314577 0012200 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class GetResponse extends Integration
{
/**
* Create a new instance
*
* @param array $options The service's required options
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
$this->endpoint = 'https://api.getresponse.com/v3';
$this->options->set('headers.X-Auth-Token', 'api-key ' . $this->key);
$this->options->set('headers.Accept-Encoding', 'gzip,deflate');
}
/**
* Subscribe user to GetResponse Campaign
*
* https://apidocs.getresponse.com/v3/resources/contacts#contacts.create
*
* TODO: Update existing contact
*
* @param string $email Email of the Contact
* @param string $name The name of the Contact
* @param int $dayOfCycle Enter 0 to add to the start day of the cycle.
* @param object $campaign Campaign ID
* @param object $customFields Collection of custom fields
* @param object $update_existing Update existing contact
* @param array $tags Set user tags
* @param string $tags_replace Determines what changes to make to the subscriber's tags. Values: add_only, replace_all
*
* @return void
*/
public function subscribe($email, $name, $campaign, $customFields, $update_existing, $dayOfCycle = 0, $tags = [], $tags_replace = 'add_only')
{
$data = [
'email' => $email,
'name' => $name,
'dayOfCycle' => $dayOfCycle,
'campaign' => ['campaignId' => $campaign],
'customFieldValues' => $this->validateCustomFields($customFields),
'ipAddress' => \NRFramework\User::getIP()
];
if (empty($name) || is_null($name))
{
unset($data['name']);
}
$contactId = null;
$service_tags = [];
if ($tags)
{
$service_tags = $this->getServiceTags();
}
// Replace all existing contact tags with new ones
if ($tags && $tags_replace === 'replace_all')
{
$data['tags'] = $this->validateTags($tags, $service_tags, $tags_replace);
}
if ($update_existing)
{
$contactId = $this->getContact($email);
}
$endpoint = 'contacts';
$endpoint = !empty($contactId) ? $endpoint . '/' . $contactId : $endpoint;
$this->post($endpoint, $data);
// Add new tags to the contact
if ($tags && $tags_replace === 'add_only' && $contactId)
{
$data = ['tags' => $this->validateTags($tags, $service_tags, $tags_replace)];
$this->post('contacts/' . $contactId . '/tags', $data);
}
}
/**
* Return all service tags.
*
* @return array
*/
private function getServiceTags()
{
$tags = [];
foreach ($this->get('tags') as $tag)
{
$tags[$tag['tagId']] = $tag['name'];
}
return $tags;
}
/**
* Validates and returns the valid tags.
*
* @param array $tags
* @param array $service_tags
*
* @return array
*/
private function validateTags($tags = [], $service_tags = [], $tags_replace = 'add_only')
{
$final_tags = [];
foreach ($tags as $index => $tag)
{
$valid = false;
// Find tag in service tags and add it to final tags list
foreach ($service_tags as $tagId => $tagName)
{
if ($tagId === $tag || $tagName === $tag)
{
$valid = true;
// Add to final list
$final_tags[] = [
'tagId' => $tagId
];
}
}
// Add invalid tags
if (!$valid && $tags_replace == 'add_only')
{
$new_tag = $this->createTag($tag);
$final_tags[] = [
'tagId' => $new_tag['tagId']
];
}
}
return $final_tags;
}
private function createTag($tag)
{
$data = [
'name' => $tag
];
return $this->post('tags', $data);
}
/**
* Returns a new array with valid only custom fields
*
* @param array $customFields Array of custom fields
*
* @return array Array of valid only custom fields
*/
public function validateCustomFields($customFields)
{
$fields = [];
if (!is_array($customFields))
{
return $fields;
}
$accountCustomFields = $this->get('custom-fields');
if (!$this->request_successful)
{
return $fields;
}
foreach ($accountCustomFields as $key => $customField)
{
if (!isset($customFields[$customField['name']]))
{
continue;
}
$fields[] = [
'customFieldId' => $customField['customFieldId'],
'value' => [$customFields[$customField['name']]]
];
}
return $fields;
}
/**
* Get the last error returned by either the network transport, or by the API.
* If something didn't work, this should contain the string describing the problem.
*
* @return string describing the error
*/
public function getLastError()
{
$body = $this->last_response->body;
if (!isset($body['context']) || !isset($body['context'][0]))
{
return $body['codeDescription'] . ' - ' . $body['message'];
}
$error = $body['context'][0];
// GetResponse returns a JSON string as $error and we try to decode it so we can return a more human-friendly error message
$error = is_string($error) && json_encode($error, true) ? json_decode($error, true) : $error;
if (is_array($error) && isset($error['fieldName']))
{
$errorFieldName = is_array($error['fieldName']) ? implode(' ', $error['fieldName']) : $error['fieldName'];
return $errorFieldName . ': ' . $error['message'];
}
return (is_array($error)) ? implode(' ', $error) : $error;
}
/**
* Returns all available GetResponse campaigns
*
* https://apidocs.getresponse.com/v3/resources/campaigns#campaigns.get.all
*
* @return array
*/
public function getLists()
{
$data = $this->get('campaigns');
if (!$this->success())
{
return;
}
if (!is_array($data) || !count($data))
{
return;
}
$lists = [];
foreach ($data as $key => $list)
{
$lists[] = [
'id' => $list['campaignId'],
'name' => $list['name']
];
}
return $lists;
}
/**
* Get the Contact resource
*
* @param string $email The email of the contact which we want to retrieve
*
* @return string The Contact ID
*/
public function getContact($email)
{
if (!isset($email))
{
return;
}
$data = $this->get('contacts', ['query[email]' => $email]);
if (empty($data))
{
return;
}
// the returned data is an array with only one contact
$contactId = $data[0]['contactId'];
return ($contactId) ? $contactId : null;
}
} Integrations/ActiveCampaign.php 0000644 00000024530 15235314577 0012616 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class ActiveCampaign extends Integration
{
/**
* Create a new instance
* @param array $options The service's required options
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
$this->setEndpoint($options['endpoint'] . '/api/3');
$this->options->set('headers.Api-Token', $this->key);
}
/**
* Subscribe user to ActiveCampaign List
*
* https://developers.activecampaign.com/v3/reference#create-contact
*
* @param string $email The Email of the Contact
* @param string $name The name of the Contact (Name can be also declared in Custom Fields)
* @param string $list List ID
* @param string $tags Tags for this contact (comma-separated). Example: "tag1, tag2, etc"
* @param array $customfields Custom Fields
* @param boolean $updateexisting Update Existing User
*
* @return void
*/
public function subscribe($email, $name, $lists, $tags = '', $customfields = [], $updateexisting = true)
{
// Detect name
$name = (is_null($name) || empty($name)) ? $this->getNameFromCustomFields($customfields) : explode(' ', $name, 2);
$apiAction = ($updateexisting) ? 'contact/sync' : 'contacts';
$data = [
'contact' => [
'email' => $email,
'phone' => $this->getPhone($customfields),
'ip4' => \NRFramework\User::getIP()
],
];
// Add first and last name only if they are not empty, as ActiveCampaign will empty the fields if they are empty.
if (isset($name[0]) && $name[0])
{
$data['contact']['firstName'] = $name[0];
}
if (isset($name[1]) && $name[1])
{
$data['contact']['lastName'] = $name[1];
}
$this->post($apiAction, $data);
if (!$this->request_successful)
{
return;
}
// Retrive the contact's ID
$contact_id = $this->getContactIDFromResponse();
// Add Lists to Contact
$this->addListsToContact($contact_id, $lists);
// Add Tags to Contact
if (!empty($tags))
{
$tags = is_array($tags) ? $tags : explode(',', $tags);
$tag_ids = $this->convertTagNamesToIDs($tags);
if ($tag_ids && !empty($tag_ids))
{
$this->addTagsToContact($tag_ids, $contact_id);
}
}
// Add Custom Fields to Contact
$this->addCustomFieldsToContact($customfields, $contact_id);
}
/**
* Returns the phone number of the contact.
*
* @param array $customfields
*
* @return string
*/
private function getPhone($customfields)
{
$phone = $this->getCustomFieldValue('phone', $customfields);
if (is_string($phone))
{
return $phone;
}
if (isset($phone['code']) && isset($phone['value']) && $phone['value'])
{
$calling_code = \NRFramework\Countries::getCallingCodeByCountryCode($phone['code']);
$calling_code = $calling_code !== '' ? '+' . $calling_code : '';
$phone = $calling_code . $phone['value'];
}
else
{
$phone = '';
}
return $phone;
}
/**
* Update Custom Field Values for a Contact
*
* API Reference: https://developers.activecampaign.com/v3/reference#fieldvalues
*
* @param array $custom_fields Array of custom field values
* @param integer $contact_id The contact's ID
*
* @return mixed Null on failure, void on success
*/
private function addCustomFieldsToContact($custom_fields, $contact_id)
{
if (empty($custom_fields))
{
return;
}
$custom_fields = array_change_key_case($custom_fields);
if (!$all_custom_fields = $this->getAllCustomFields())
{
return;
}
foreach ($custom_fields as $custom_field_key => $custom_field_value)
{
if (empty($custom_field_value))
{
continue;
}
$custom_field = strtolower(trim($custom_field_key));
if (!array_key_exists($custom_field, $all_custom_fields))
{
continue;
}
// Let's add Custom Field to our contact
$custom_field_data = $all_custom_fields[$custom_field];
// Radio buttons expect a string. Not an array.
if ($custom_field_data['type'] == 'checkbox' && is_array($custom_field_value))
{
$custom_field_value = implode('||', $custom_field_value);
$custom_field_value = '||' . $custom_field_value . '||';
}
$this->post('fieldValues', [
'fieldValue' => [
'contact' => $contact_id,
'field' => $custom_field_data['id'],
'value' => $custom_field_value
]
]);
}
}
/**
* Add tags to contact
*
* API Reference: https://developers.activecampaign.com/v3/reference#create-contact-tag
*
* @param array $tag_ids Array of tag IDs
* @param integer $contact_id The contact's ID
*
* @return void
*/
private function addTagsToContact($tag_ids, $contact_id)
{
foreach ($tag_ids as $tag_id)
{
$this->post('contactTags', [
'contactTag' => [
'contact' => $contact_id,
'tag' => $tag_id,
]
]);
}
}
/**
* Convert a list of tag names to tag IDs
*
* @param array $tags Array ot tag names
*
* @return mixed Null on failure, assosiative tag name-based array on success.
*/
private function convertTagNamesToIDs($tags)
{
if (!$account_tags = $this->getAllTags())
{
return;
}
$account_tags = array_map('strtolower', $account_tags);
$tag_ids = [];
foreach ($tags as $tag)
{
if (empty($tag))
{
continue;
}
$tag = strtolower(trim($tag));
if (!$tag_id = array_search($tag, $account_tags))
{
continue;
}
$tag_ids[] = $tag_id;
}
return $tag_ids;
}
/**
* Retrieve all contact-based tags
*
* API Reference: https://developers.activecampaign.com/v3/reference#list-all-tasks
*
* @return mixed Null on failure, assosiative array on success
*/
private function getAllTags()
{
$tags = $this->get('tags');
if (!$tags || !is_array($tags) || !isset($tags['tags']))
{
return;
}
$tags_ = [];
foreach ($tags['tags'] as $tag)
{
if ($tag['tagType'] != 'contact')
{
continue;
}
$tags_[$tag['id']] = $tag['tag'];
}
return $tags_;
}
/**
* Add lists to contact
*
* @param integer $contact_id The Active Campaign Contact ID
* @param mixed $lists The list ID to add the contact to.
*
* @return void
*/
private function addListsToContact($contact_id, $lists)
{
$lists = is_array($lists) ? $lists : explode(',', $lists);
foreach ($lists as $list)
{
$this->post('contactLists', [
'contactList' => [
'list' => $list,
'contact' => $contact_id,
'status' => 1
]
]);
}
}
/**
* Determine the newly created contact's ID
*
* @return string
*/
private function getContactIDFromResponse()
{
$response = $this->last_response;
if (isset($response->body) && isset($response->body['contact']) && isset($response->body['contact']['id']))
{
return $response->body['contact']['id'];
}
}
/**
* Search for First Name and Last Name in Custom Fields and return an array with both values.
*
* @param array $customfields The Custom Fields array passed by the user.
*
* @return array
*/
private function getNameFromCustomFields($customfields)
{
return [
(string) $this->getCustomFieldValue(['first_name', 'First Name'], $customfields),
(string) $this->getCustomFieldValue(['last_name', 'Last Name'], $customfields)
];
}
/**
* Retrieve all account lists
*
* API Reference: https://developers.activecampaign.com/v3/reference#retrieve-all-lists
*
* @return mixed Null on failure, Array on success
*/
public function getLists()
{
$data = $this->get('lists');
if (!$data || !isset($data['lists']) || count($data['lists']) == 0)
{
return;
}
$lists = [];
foreach ($data['lists'] as $list)
{
$lists[] = [
'id' => $list['id'],
'name' => $list['name']
];
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* API Reference: https://developers.activecampaign.com/v3/reference#errors
*
* @return string
*/
public function getLastError()
{
$error_code = $this->last_response->code;
$error_message = 'Active Campaign Error';
switch ((int) $error_code)
{
case 403:
$error_message = 'The request could not be authenticated or the authenticated user is not authorized to access the requested resource.';
break;
case 404:
$error_message = 'The requested resource does not exist.';
break;
case 422:
$error_message = 'The request could not be processed, usually due to a missing or invalid parameter.';
if (isset($this->last_response->body['errors']) && isset($this->last_response->body['errors'][0]))
{
$error_message = $this->last_response->body['errors'][0]['title'];
}
break;
}
return $error_message;
}
/**
* Returns the Active Campaign Account's Custom Fields
*
* API Reference: https://developers.activecampaign.com/v3/reference#retrieve-fields-1
*
* @return array
*/
public function getAllCustomFields()
{
$fields = $this->get('fields');
if (!$fields || !isset($fields['fields']))
{
return;
}
// Make our life easier by creating a title-based assosiative array
$f = [];
foreach ($fields['fields'] as $key => $field)
{
if (!$field || !isset($field['title']))
{
continue;
}
$key = strtolower(trim($field['title']));
$f[$key] = $field;
}
return $f;
}
/**
* Make an HTTP GET request for retrieving data.
*
* ActiveCampaign has a limit of max 100 results per page.
* https://developers.activecampaign.com/reference#pagination
*
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
*
* @return array|false Assoc array of API response, decoded from JSON
*/
public function get($method, $args = array())
{
$args['limit'] = isset($args['limit']) ? $args['limit'] : 100;
$args['offset'] = isset($args['offset']) ? $args['offset'] : 0;
$response = parent::get($method, $args);
if ($args['offset'] < (int) $response['meta']['total'])
{
$args['offset'] += $args['limit'];
$response_next = $this->get($method, $args);
$response[$method] = array_merge($response[$method], $response_next[$method]);
}
return $response;
}
} Integrations/Brevo.php 0000644 00000004522 15235314577 0011017 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class Brevo extends Integration
{
/**
* Create a new instance
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
$this->setEndpoint('https://api.brevo.com/v3');
$this->options->set('headers.api-key', $this->key);
}
/**
* Subscribes a user to a Brevo Account
*
* API Reference v3:
* https://developers.brevo.com/reference/createcontact
*
* @param string $email The user's email
* @param array $params All the form fields
* @param string $listid The List ID
* @param boolean $update_existing Whether to update the existing contact (Only in v3)
*
* @return boolean
*/
public function subscribe($email, $params, $listid = false, $update_existing = true)
{
$data = [
'email' => $email,
'attributes' => (object) $params,
'updateEnabled' => $update_existing
];
if ($listid)
{
$data['listIds'] = [(int) $listid];
}
$this->post('contacts', $data);
return true;
}
/**
* Returns all Campaign lists
*
* API Reference v3:
* https://developers.brevo.com/reference/getlists-1
*
* @return array
*/
public function getLists()
{
$data = [
'offset' => 0,
'limit' => 50
];
$lists = [];
$data = $this->get('contacts/lists', $data);
// sanity check
if (!isset($data['lists']) || !is_array($data['lists']) || $data['count'] == 0)
{
return $lists;
}
foreach ($data['lists'] as $key => $list)
{
$lists[] = [
'id' => $list['id'],
'name' => $list['name']
];
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* API Reference:
* https://developers.brevo.com/docs/how-it-works#error-codes
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (!isset($body['code']))
{
return $message;
}
return $body['message'];
}
} Integrations/HCaptcha.php 0000644 00000004717 15235314577 0011423 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/**
* The HCaptcha Wrapper
*/
class HCaptcha extends Integration
{
/**
* Service Endpoint
*
* @var string
*/
protected $endpoint = 'https://hcaptcha.com/siteverify';
/**
* Create a new instance
*
* @param array $options
*
* @throws \Exception
*/
public function __construct($options = [])
{
parent::__construct();
if (!array_key_exists('secret', $options))
{
$this->setError('NR_RECAPTCHA_INVALID_SECRET_KEY');
throw new \Exception($this->getLastError());
}
$this->setKey($options['secret']);
}
/**
* Calls the hCaptcha siteverify API to verify whether the user passes hCaptcha test.
*
* @param string $response Response string from hCaptcha verification.
* @param string $remoteip IP address of end user
*
* @return bool Returns true if the user passes hCaptcha test
*/
public function validate($response, $remoteip = null)
{
if (empty($response) || is_null($response))
{
return $this->setError('NR_RECAPTCHA_PLEASE_VALIDATE');
}
// remove these headers in order for hCaptcha to be abl to process the request
$this->options->remove('headers.Accept');
$this->options->remove('headers.Content-Type');
// do not encode request
$this->setEncode(false);
$data = [
'secret' => $this->key,
'response' => $response,
];
$this->post('', $data);
return true;
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$success = parent::determineSuccess();
$body = $this->last_response->body;
if ($body['success'] == false && array_key_exists('error-codes', $body) && count($body['error-codes']) > 0)
{
$success = $this->setError(implode(', ', $body['error-codes']));
}
return ($this->request_successful = $success);
}
/**
* Set wrapper error text
*
* @param String $error The error message to display
*/
private function setError($error)
{
$this->last_error = Text::_('NR_HCAPTCHA') . ': ' . Text::_($error);
return false;
}
} Integrations/CampaignMonitor.php 0000644 00000010371 15235314577 0013030 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class CampaignMonitor extends Integration
{
/**
* Create a new instance
*
* @param array $options The service's required options
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options);
$this->setEndpoint('https://api.createsend.com/api/v3.1');
$this->options->set('userauth', $this->key);
$this->options->set('passwordauth', 'nopass');
}
/**
* Subscribe user to Campaign Monitor
*
* API References:
* https://www.campaignmonitor.com/api/subscribers/#importing_many_subscribers
* Reminder:
* The classic add_subscriber method of Campaign Monitor's API is NOT instantaneous!
* It is suggested to use their import method for instantaneous subscriptions!
*
* @param string $email User's email address
* @param string $name User's Name
* @param string $list The Campaign Monitor list unique ID
* @param array $custom_fields Custom Fields
*
* @return void
*/
public function subscribe($email, $name, $list, $customFields = array())
{
$data = array(
'Subscribers' => array(
array(
'EmailAddress' => $email,
'Name' => $name,
'Resubscribe' => true,
),
),
);
if (is_array($customFields) && count($customFields))
{
$data['Subscribers'][0]['CustomFields'] = $this->validateCustomFields($customFields, $list);
}
$this->post('subscribers/' . $list . '/import.json', $data);
return true;
}
/**
* Returns a new array with valid only custom fields
*
* @param array $formCustomFields Array of custom fields
*
* @return array Array of valid only custom fields
*/
public function validateCustomFields($formCustomFields, $list)
{
$fields = array();
if (!is_array($formCustomFields))
{
return $fields;
}
$listCustomFields = $this->get('lists/' . $list . '/customfields.json');
if (!$this->request_successful)
{
return $fields;
}
$formCustomFieldsKeys = array_keys($formCustomFields);
foreach ($listCustomFields as $listCustomField)
{
$field_name = $listCustomField['FieldName'];
if (!in_array($field_name, $formCustomFieldsKeys))
{
continue;
}
$value = $formCustomFields[$field_name];
// Always convert custom field value to array, to support multiple values in a custom field.
$value = is_array($value) ? $value : (array) $value;
foreach ($value as $val)
{
$fields[] = array(
'Key' => $field_name,
'Value' => $val,
);
}
}
return $fields;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (isset($body['Message']))
{
$message = $body['Message'];
}
if (isset($body['ResultData']['FailureDetails'][0]['Message']))
{
$message .= ' - ' . $body['ResultData']['FailureDetails'][0]['Message'];
}
return $message;
}
/**
* Returns all Client lists
*
* https://www.campaignmonitor.com/api/clients/#getting-subscriber-lists
*
* @return array
*/
public function getLists()
{
$clients = $this->getClients();
if (!is_array($clients))
{
return;
}
$lists = array();
foreach ($clients as $key => $client)
{
if (!isset($client['ClientID']))
{
continue;
}
$clientLists = $this->get('/clients/' . $client['ClientID'] . '/lists.json');
if (!is_array($clientLists))
{
continue;
}
foreach ($clientLists as $key => $clientList)
{
$lists[] = array(
'id' => $clientList['ListID'],
'name' => $clientList['Name']
);
}
}
return $lists;
}
/**
* Get Clients
*
* https://www.campaignmonitor.com/api/account/
*
* @return mixed Array on success, Null on fail
*/
private function getClients()
{
$clients = $this->get('/clients.json');
if (!$this->success())
{
return;
}
return $clients;
}
} Integrations/Zoho.php 0000644 00000007025 15235314577 0010662 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class Zoho extends Integration
{
/**
* Create a new instance
*
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options['api']);
$this->endpoint = 'https://campaigns.zoho.com/api';
}
/**
* Subscribe user to ZoHo
*
* https://www.zoho.com/campaigns/help/api/contact-subscribe.html
*
* @param string $email User's email address
* @param string $list The ZoHo list unique ID
* @param Object $customFields Collection of custom fields
*
* @return void
*/
public function subscribe($email, $list, $customFields = array())
{
$contactinfo = json_encode(array_merge(array("Contact Email" => $email), $customFields));
$data = array(
"authtoken" => $this->key,
"scope" => "CampaignsAPI",
"version" => "1",
"resfmt" => "JSON",
"listkey" => $list,
"contactinfo" => $contactinfo
);
$this->get('json/listsubscribe', $data);
return true;
}
/**
* Returns all available ZoHo lists
*
* https://www.zoho.com/campaigns/help/api/get-mailing-lists.html
*
* @return array
*/
public function getLists()
{
if (!$this->key)
{
return;
}
$data = array(
'authtoken' => $this->key,
'scope' => 'CampaignsAPI',
'sort' => 'asc',
'resfmt' => 'JSON',
'range' => '1000' //ambiguously large range of total results to overwrite the default range which is 20
);
$data = $this->get("getmailinglists", $data);
if (!$this->success())
{
return;
}
$lists = array();
if (!isset($data["list_of_details"]) || !is_array($data["list_of_details"]))
{
return $lists;
}
foreach ($data["list_of_details"] as $key => $list)
{
$lists[] = array(
"id" => $list["listkey"],
"name" => $list["listname"]
);
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
if (isset($body['message']))
{
return $body['message'];
}
return 'An unspecified error occured';
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$status = $this->findHTTPStatus();
// check if the status is equal to the arbitrary success codes of ZoHo
if (in_array($status, array(0, 200, 6101, 6201)))
{
return ($this->request_successful = true);
}
return false;
}
/**
* Find the HTTP status code from the headers or API response body
*
* @return int HTTP status code
*/
protected function findHTTPStatus()
{
$status = $this->last_response->code;
$success = ($status >= 200 && $status <= 299) ? true : false;
if (!$success)
{
return 418;
}
// ZoHo sometimes uses "Code" instead of "code"
// also they don't use HTTP status codes
// instead they store their own status code inside the response body
$data = array_change_key_case($this->last_response->body);
if (isset($data['code']))
{
return (int) $data['code'];
}
return 418;
}
} Integrations/SendInBlue.php 0000644 00000004321 15235314577 0011727 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
class SendInBlue extends Integration
{
/**
* Create a new instance
* @param array $options The service's required options
* @throws \Exception
*/
public function __construct($options)
{
parent::__construct();
$this->setKey($options['api']);
$this->setEndpoint('https://api.sendinblue.com/v2.0');
$this->options->set('headers.api-key', $this->key);
}
/**
* Subscribes a user to a SendinBlue Account
*
* API Reference:
* https://apidocs.sendinblue.com/user/#1
*
* @param string $email The user's email
* @param array $params All the form fields
* @param string $listid The List ID
*
* @return boolean
*/
public function subscribe($email, $params, $listid = false)
{
$data = array(
'email' => $email,
'attributes' => $params,
);
if ($listid)
{
$data['listid'] = array($listid);
}
$this->post('user/createdituser', $data);
return true;
}
/**
* Returns all Campaign lists
*
* https://apidocs.sendinblue.com/list/#1
*
* @return array
*/
public function getLists()
{
$data = array(
'page' => 1,
'page_limit' => 50
);
$lists = array();
$data = $this->get('/list', $data);
if (!isset($data['data']['lists']) || !is_array($data['data']['lists']) || $data['data']['total_list_records'] == 0)
{
return $lists;
}
foreach ($data['data']['lists'] as $key => $list)
{
$lists[] = array(
'id' => $list['id'],
'name' => $list['name']
);
}
return $lists;
}
/**
* Get the last error returned by either the network transport, or by the API.
*
* API Reference:
* https://apidocs.sendinblue.com/response/
*
* @return string
*/
public function getLastError()
{
$body = $this->last_response->body;
$message = '';
if (isset($body['code']) && ($body['code'] == 'failure'))
{
$message = $body['message'];
}
return $message;
}
} Integrations/Integration.php 0000644 00000020254 15235314577 0012225 0 ustar 00 <?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Integrations;
// No direct access
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
use Joomla\CMS\Http\HttpFactory;
class Integration
{
protected $key;
protected $endpoint;
protected $request_successful = false;
protected $last_error = '';
protected $last_response = [];
protected $last_request = [];
protected $timeout = 60;
protected $options;
protected $encode = true;
protected $response_type = 'json';
public function __construct()
{
$this->options = new Registry;
$this->options->set('timeout', $this->timeout);
$this->options->set('headers.Accept', 'application/json');
$this->options->set('headers.Content-Type', 'application/json');
}
/**
* Setter method for the API Key or Access Token
*
* @param string $apiKey
*/
public function setKey($apiKey)
{
$apiKey = is_array($apiKey) && isset($apiKey['api']) ? $apiKey['api'] : $apiKey;
if (!is_string($apiKey) || empty($apiKey) || is_null($apiKey))
{
throw new \Exception('Invalid API Key supplied.');
}
$this->key = trim($apiKey);
}
/**
* Setter method for the endpoint
* @param string $url The URL which is set in the account's developer settings
* @throws \Exception
*/
public function setEndpoint($url)
{
if (!empty($url))
{
$this->endpoint = $url;
}
else
{
throw new \Exception("Invalid Endpoint URL `{$url}` supplied.");
}
}
/**
* Was the last request successful?
* @return bool True for success, false for failure
*/
public function success()
{
return $this->request_successful;
}
/**
* Get the last error returned by either the network transport, or by the API.
* If something didn't work, this should contain the string describing the problem.
* @return array|false describing the error
*/
public function getLastError()
{
return $this->last_error ?: false;
}
/**
* Get an array containing the HTTP headers and the body of the API response.
* @return array Assoc array with keys 'headers' and 'body'
*/
public function getLastResponse()
{
return $this->last_response;
}
/**
* Get an array containing the HTTP headers and the body of the API request.
* @return array Assoc array
*/
public function getLastRequest()
{
return $this->last_request;
}
/**
* Make an HTTP DELETE request - for deleting data
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (if any)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function delete($method, $args = [])
{
return $this->makeRequest('delete', $method, $args);
}
/**
* Make an HTTP GET request - for retrieving data
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function get($method, $args = [])
{
return $this->makeRequest('get', $method, $args);
}
/**
* Make an HTTP PATCH request - for performing partial updates
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function patch($method, $args = [])
{
return $this->makeRequest('patch', $method, $args);
}
/**
* Make an HTTP POST request - for creating and updating items
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function post($method, $args = [])
{
return $this->makeRequest('post', $method, $args);
}
/**
* Make an HTTP PUT request - for creating new items
* @param string $method URL of the API request method
* @param array $args Assoc array of arguments (usually your data)
* @return array|false Assoc array of API response, decoded from JSON
*/
public function put($method, $args = [])
{
return $this->makeRequest('put', $method, $args);
}
/**
* Performs the underlying HTTP request. Not very exciting.
* @param string $http_verb The HTTP verb to use: get, post, put, patch, delete
* @param string $method The API method to be called
* @param array $args Assoc array of parameters to be passed
* @return array|false Assoc array of decoded result
* @throws \Exception
*/
protected function makeRequest($http_verb, $method, $args = [])
{
$url = $this->endpoint;
if (!empty($method) && !is_null($method) && strpos($url, '?') === false)
{
$url .= '/' . $method;
}
$this->last_error = '';
$this->request_successful = false;
$this->last_response = [];
$this->last_request = [
'method' => $http_verb,
'path' => $method,
'url' => $url,
'body' => '',
'timeout' => $this->timeout,
];
$http = HttpFactory::getHttp($this->options);
switch ($http_verb)
{
case 'post':
$this->attachRequestPayload($args);
$response = $http->post($url, $this->last_request['body']);
break;
case 'get':
$query = http_build_query($args, '', '&');
$this->last_request['body'] = $query;
$response = (strpos($url,'?') !== false) ? $http->get($url . '&' . $query) : $http->get($url . '?' . $query);
break;
case 'delete':
$response = $http->delete($url);
break;
case 'patch':
$this->attachRequestPayload($args);
$response = $http->patch($url, $this->last_request['body']);
break;
case 'put':
$this->attachRequestPayload($args);
$response = $http->put($url, $this->last_request['body']);
break;
}
// Do not touch directly the $response object to prevent the PHP 8.2 "Creation of dynamic property" deprecation notice.
$this->last_response = (object) [
'body' => $this->convertResponse($response->body),
'headers' => $response->headers,
'code' => $response->code
];
$this->determineSuccess();
return $this->last_response->body;
}
/**
* Encode the data and attach it to the request
* @param array $data Assoc array of data to attach
*/
protected function attachRequestPayload($data)
{
if (!$this->encode)
{
$this->last_request['body'] = http_build_query($data);
return;
}
$this->last_request['body'] = json_encode($data);
}
/**
* Check if the response was successful or a failure. If it failed, store the error.
*
* @return bool If the request was successful
*/
protected function determineSuccess()
{
$status = $this->last_response->code;
$success = ($status >= 200 && $status <= 299) ? true : false;
return ($this->request_successful = $success);
}
/**
* Converts the HTTP Call response to a traversable type
*
* @param json|xml $response
*
* @return array|object
*/
protected function convertResponse($response)
{
switch ($this->response_type)
{
case 'json':
return json_decode($response, true);
case 'xml':
return new \SimpleXMLElement($response);
case 'text':
return $response;
}
}
/**
* Search Custom Fields declared by the user for a specific custom field. If exists return its value.
*
* @param array $needles The custom field names
* @param array $haystack The custom fields array
*
* @return string The value of the custom field or an empty string if not found
*/
protected function getCustomFieldValue($needles, $haystack)
{
$needles = is_array($needles) ? $needles : (array) $needles;
$haystack = array_change_key_case($haystack);
$found = '';
foreach ($needles as $needle)
{
$needle = strtolower($needle);
if (array_key_exists($needle, $haystack))
{
$found = is_string($haystack[$needle]) ? trim($haystack[$needle]) : $haystack[$needle];
break;
}
}
return $found;
}
/**
* Set encode
*
* @param boolean $encode
*
* @return void
*/
public function setEncode($encode)
{
$this->encode = $encode;
}
}