Your IP : 216.73.216.224


Current Path : /var/www/html/libraries/noboss/src/Util/
Upload File :
Current File : /var/www/html/libraries/noboss/src/Util/NbHtmlContentUtil.php

<?php
/**
 * @package			No Boss Extensions
 * @subpackage  	No Boss Library
 * @author			No Boss Technology <contact@nobosstechnology.com>
 * @copyright		Copyright (C) 2026 No Boss Technology. All rights reserved.
 * @license			GNU Lesser General Public License version 3 or later; see <https://www.gnu.org/licenses/lgpl-3.0.en.html>
 */

namespace Noboss\Library\Util;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Utilitario para preparacao de HTML de conteudo importado (ICS, Google Calendar, etc.).
 */
abstract class NbHtmlContentUtil
{
    /**
     * Tags permitidas no conteudo da modal (PHP 8.2+ inclui atributo href em anchor).
     */
    public const ALLOWED_MODAL_TAGS = '<b><a href><i><br><ul><ol><li>';

    /**
     * Prepara HTML seguro para exibicao na modal a partir de descricao plana e/ou HTML alternativo.
     *
     * @param   string       $plain     Descricao em texto plano (DESCRIPTION do ICS).
     * @param   string|null  $htmlAlt   HTML alternativo (ex.: X-ALT-DESC), quando disponivel.
     *
     * @return  string
     */
    public static function prepareImportedModalContent(string $plain, ?string $htmlAlt = null): string
    {
        if ($htmlAlt !== null && trim($htmlAlt) !== '') {
            $content = html_entity_decode(trim($htmlAlt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
            $content = self::flattenAnchorsForLinkify($content);
            $content = self::unwrapAngleBracketUrls($content);
            $content = strip_tags($content, self::ALLOWED_MODAL_TAGS);

            return self::linkifyPlainUrls($content);
        }

        $plain = trim($plain);
        if ($plain === '') {
            return '';
        }

        $content = html_entity_decode($plain, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $content = self::unwrapAngleBracketUrls($content);
        $content = nl2br($content);
        $content = strip_tags($content, self::ALLOWED_MODAL_TAGS);

        return self::linkifyPlainUrls($content);
    }

    /**
     * Remove o rodape promocional dos calendarios publicos de feriados do Google Calendar.
     *
     * Ex.: "Data comemorativa\nPara ocultar as datas comemorativas, acesse Configuracoes
     * do Google Agenda > Feriados no Brasil" → "Data comemorativa"
     *
     * @param   string  $text  Descricao plana ou HTML (DESCRIPTION / X-ALT-DESC).
     *
     * @return  string
     */
    public static function stripGoogleHolidayBoilerplate(string $text): string
    {
        $text = trim($text);

        if ($text === '') {
            return '';
        }

        $plain = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $plain = preg_replace('/<br\s*\/?>/i', ' ', $plain) ?? $plain;
        $plain = strip_tags($plain);
        $plain = preg_replace('/\s+/u', ' ', $plain) ?? $plain;
        $plain = trim($plain);

        if ($plain === '') {
            return '';
        }

        $googleMarker = '/(?:Google\s+(?:Agenda|Calendar|Kalender)|Agenda\s+(?:de|do)\s+Google|Calendrier\s+Google)/iu';

        if (preg_match($googleMarker, $plain) !== 1) {
            return $text;
        }

        $hideIntro = implode(
            '|',
            [
                'Para ocultar(?: as)? datas comemorativas',
                'Para ocultar los d[ií]as festivos',
                'Para ocultar las fechas festivas',
                'Para esconder os feriados',
                'To hide observances',
                'To hide(?: public)? holidays',
                'Pour masquer les jours f[eé]ri[eé]s',
                'So blenden Sie Feiertage',
                'Per nascondere (?:i )?giorni festivi',
                'Om feestdagen te verbergen',
                'Aby ukry[cć] [śs]wi[eę]ta',
                'Skjul helligdager',
                'Piilota juhlapyh[aä]t',
                'Ukryj [śs]wi[eę]ta',
                'Feiertage ausblenden',
            ]
        );

        if (preg_match('/^(.*?)\s*(?:' . $hideIntro . ')/iu', $plain, $matches) === 1) {
            $kept = rtrim(trim($matches[1]), '.');

            return $kept !== '' ? $kept : $text;
        }

        return $text;
    }

    /**
     * Converte URLs entre angulos (ex.: Teams "<https://...>") em URL plana antes do strip_tags.
     *
     * @param   string   $text
     *
     * @return  string
     */
    protected static function unwrapAngleBracketUrls(string $text): string
    {
        $result = preg_replace(
            '/<((?:https?|mailto):[^>\s]+)>/i',
            ' $1',
            $text
        );

        return $result ?? $text;
    }

    /**
     * Expoe texto e URL de anchors existentes para o linkify reconstruir links clicaveis.
     *
     * @param   string   $html
     *
     * @return  string
     */
    protected static function flattenAnchorsForLinkify(string $html): string
    {
        $result = preg_replace(
            '/<a\b[^>]*href=[\'"]([^\'"]+)[\'"][^>]*>(.*?)<\/a>/is',
            '$2 $1',
            $html
        );

        return $result ?? $html;
    }

    /**
     * Converte URLs em texto plano em links clicaveis, preservando anchors existentes.
     *
     * @param   string   $text   Texto ou HTML parcial.
     *
     * @return  string
     */
    public static function linkifyPlainUrls(string $text): string
    {
        if ($text === '') {
            return '';
        }

        $parts = preg_split('/(<a\b[^>]*>.*?<\/a>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
        if ($parts === false) {
            return $text;
        }

        $urlPattern = '/\b(?:(https?):\/\/[^\s<>"\'\)\]]+|mailto:[^\s<>"\'\)\]]+)/i';

        foreach ($parts as $index => $part) {
            if ($part === '' || preg_match('/^<a\b/i', $part) === 1) {
                continue;
            }

            $parts[$index] = preg_replace_callback(
                $urlPattern,
                [self::class, 'buildLinkFromUrlMatch'],
                $part
            ) ?? $part;
        }

        return implode('', $parts);
    }

    /**
     * Callback do preg_replace_callback para montar um anchor a partir de URL detectada.
     *
     * @param   array   $matches   Grupos capturados pela regex.
     *
     * @return  string
     */
    protected static function buildLinkFromUrlMatch(array $matches): string
    {
        $url = $matches[0];

        // Remove pontuacao final comum fora da URL (ex.: "https://exemplo.com).")
        $trailing = '';
        if (preg_match('/^(.+?)([.,;:!?]+)$/u', $url, $trimMatch) === 1) {
            $url      = $trimMatch[1];
            $trailing = $trimMatch[2];
        }

        if (!self::isAllowedUrlScheme($url)) {
            return $matches[0];
        }

        $escaped = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');

        return "<a href='{$escaped}' target='_blank' rel='noopener noreferrer'>{$escaped}</a>{$trailing}";
    }

    /**
     * Valida se a URL possui esquema permitido (http, https ou mailto).
     *
     * @param   string   $url
     *
     * @return  bool
     */
    protected static function isAllowedUrlScheme(string $url): bool
    {
        if (stripos($url, 'mailto:') === 0) {
            return true;
        }

        $scheme = parse_url($url, PHP_URL_SCHEME);

        return in_array(strtolower((string) $scheme), ['http', 'https'], true);
    }
}