| Current Path : /var/www/html/libraries/noboss/src/Api/ |
| 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;
}
}