Your IP : 216.73.216.224


Current Path : /var/www/html/libraries/noboss/src/Api/
Upload File :
Current File : /var/www/html/libraries/noboss/src/Api/NbIcsFeedApi.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\Api;

use Joomla\CMS\Language\Text;
use Noboss\Library\Util\NbCurlUtil;
use Noboss\Library\Util\NbIcsParserUtil;

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

/**
 * API para consumo de calendarios externos via URL ICS (iCalendar / RFC 5545).
 *
 * Espelha o papel de NbGoogleCalendarApi: busca o feed via NbCurlUtil e delega o parsing
 * para NbIcsParserUtil, retornando um array normalizado de eventos.
 */
abstract class NbIcsFeedApi
{
    /**
     * Busca um feed ICS por URL e retorna os eventos normalizados.
     *
     * @param   string        $url       URL do feed (http(s):// ou webcal://)
     * @param   string|null   $timezone  Timezone destino (default: offset do Joomla)
     * @param   array         $options   Opcoes repassadas ao parser (horizon_end, max_occurrences)
     *
     * @return  array   Lista de eventos normalizados
     *
     * @throws  \Exception   Em caso de URL invalida ou falha na requisicao
     */
    public static function fetchFeed($url, $timezone = null, array $options = [])
    {
        return self::fetchFeedData($url, $timezone, $options)['events'];
    }

    /**
     * Busca um feed ICS por URL e retorna nome do calendario + eventos normalizados.
     *
     * @param   string        $url       URL do feed (http(s):// ou webcal://)
     * @param   string|null   $timezone  Timezone destino (default: offset do Joomla)
     * @param   array         $options   Opcoes repassadas ao parser (horizon_end, max_occurrences)
     *
     * @return  array   ['calendar_name' => ?string, 'events' => array]
     *
     * @throws  \Exception   Em caso de URL invalida ou falha na requisicao
     */
    public static function fetchFeedData($url, $timezone = null, array $options = [])
    {
        $content = self::requestFeed($url);

        return self::parseContent($content, $timezone, $options);
    }

    /**
     * Parseia conteudo ICS bruto (sem HTTP) e retorna nome do calendario + eventos normalizados.
     *
     * Usado pela migracao via arquivo uploadado: evita duplicar a logica de parseContent
     * que tambem serve ao fetchFeedData (feed por URL).
     *
     * @param   string        $content   Conteudo bruto do arquivo/feed iCalendar
     * @param   string|null   $timezone  Timezone destino (default: offset do Joomla)
     * @param   array         $options   Opcoes repassadas ao parser (horizon_end, max_occurrences)
     *
     * @return  array   ['calendar_name' => ?string, 'events' => array]
     */
    public static function parseContent(string $content, ?string $timezone = null, array $options = []): array
    {
        return [
            'calendar_name' => NbIcsParserUtil::extractCalendarName($content),
            'events'        => NbIcsParserUtil::parse($content, $timezone, $options),
        ];
    }

    /**
     * Valida uma URL de feed ICS (usado pelo botao "testar feed").
     *
     * Nao lanca excecao: retorna sempre um array estruturado com o resultado.
     *
     * @param   string        $url       URL do feed
     * @param   string|null   $timezone  Timezone destino
     *
     * @return  array   ['success' => bool, 'message' => string, 'count' => int, 'sample' => array]
     */
    public static function validateFeed($url, $timezone = null)
    {
        $url = \trim((string) $url);

        if ($url === '') {
            return [
                'success' => false,
                'message' => Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_EMPTY_URL'),
                'count'   => 0,
                'sample'  => [],
            ];
        }

        try {
            $content = self::requestFeed($url);
        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage(),
                'count'   => 0,
                'sample'  => [],
            ];
        }

        // Verifica se o conteudo parece um arquivo iCalendar valido
        if (!self::looksLikeIcs($content)) {
            return [
                'success' => false,
                'message' => Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_INVALID_CONTENT'),
                'count'   => 0,
                'sample'  => [],
            ];
        }

        $events = NbIcsParserUtil::parse($content, $timezone);

        if (empty($events)) {
            return [
                'success' => false,
                'message' => Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_NO_EVENTS'),
                'count'   => 0,
                'sample'  => [],
            ];
        }

        // Monta uma pequena amostra para feedback ao usuario
        $sample = [];
        foreach (\array_slice($events, 0, 5) as $event) {
            $sample[] = [
                'title' => $event['event_title'],
                'date'  => $event['initial_date'],
            ];
        }

        return [
            'success' => true,
            'message' => Text::sprintf('LIB_NOBOSS_API_ICSFEED_SUCCESS', \count($events)),
            'count'   => \count($events),
            'sample'  => $sample,
        ];
    }

    /**
     * Executa a requisicao do feed e devolve o corpo bruto.
     *
     * @param   string   $url   URL do feed
     *
     * @return  string   Conteudo bruto do feed
     *
     * @throws  \Exception   Em caso de URL invalida ou falha na requisicao
     */
    protected static function requestFeed($url)
    {
        $url = self::normalizeUrl($url);

        if ($url === '' || \filter_var($url, FILTER_VALIDATE_URL) === false) {
            throw new \Exception(Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_INVALID_URL'));
        }

        $headers = [
            'Accept' => 'text/calendar, text/plain, */*',
        ];

        $response = NbCurlUtil::request('GET', $url, null, $headers);

        if (empty($response->success)) {
            if (!empty($response->httpCode) && (int) $response->httpCode === 404) {
                throw new \Exception(Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_NOT_FOUND'));
            }

            $message = !empty($response->message) ? $response->message : Text::_('LIB_NOBOSS_API_ICSFEED_ERROR_REQUEST');
            throw new \Exception($message);
        }

        return (string) ($response->data ?? '');
    }

    /**
     * Normaliza a URL do feed (webcal/webcals -> http(s), trim).
     *
     * @param   string   $url
     *
     * @return  string
     */
    protected static function normalizeUrl($url)
    {
        $url = \trim((string) $url);

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

        // webcal:// e webcals:// sao apenas convencoes de "assinatura": apontam para http/https
        if (\stripos($url, 'webcals://') === 0) {
            $url = 'https://' . \substr($url, \strlen('webcals://'));
        } elseif (\stripos($url, 'webcal://') === 0) {
            $url = 'https://' . \substr($url, \strlen('webcal://'));
        }

        return $url;
    }

    /**
     * Heuristica simples para detectar se o conteudo eh um arquivo iCalendar.
     *
     * @param   string   $content
     *
     * @return  bool
     */
    public static function looksLikeIcs($content)
    {
        if (!\is_string($content) || $content === '') {
            return false;
        }

        return \stripos($content, 'BEGIN:VCALENDAR') !== false
            || \stripos($content, 'BEGIN:VEVENT') !== false;
    }
}