diff --git a/README.md b/README.md index a5660fa..3b963a9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel. -It allows you to manage TRMNL devices, generate screens using native plugins, recipes (55+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core). With over 20k downloads and 100+ stars, it’s the most popular community-driven BYOS. +It allows you to manage TRMNL devices, generate screens using native plugins, recipes (55+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core). With over 15k downloads and 100+ stars, it’s the most popular community-driven BYOS. ![Screenshot](README_byos-screenshot.png) ![Screenshot](README_byos-screenshot-dark.png) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index dfeb757..b372cdd 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -14,7 +14,6 @@ use App\Liquid\Tags\TemplateTag; use Exception; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Http; @@ -23,7 +22,6 @@ use Illuminate\Support\Str; use Keepsuit\LaravelLiquid\LaravelLiquidExtension; use Keepsuit\Liquid\Exceptions\LiquidException; use Keepsuit\Liquid\Extensions\StandardExtension; -use SimpleXMLElement; class Plugin extends Model { @@ -85,7 +83,7 @@ class Plugin extends Model $currentValue = $this->configuration[$fieldKey] ?? null; // If the field has a default value and no current value is set, it's not missing - if ((in_array($currentValue, [null, '', []], true)) && ! isset($field['default'])) { + if (($currentValue === null || $currentValue === '' || ($currentValue === [])) && ! isset($field['default'])) { return true; // Found a required field that is not set and has no default } } @@ -147,9 +145,11 @@ class Plugin extends Model try { // Make the request based on the verb - $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl); - - $response = $this->parseResponse($httpResponse); + if ($this->polling_verb === 'post') { + $response = $httpRequest->post($resolvedUrl)->json(); + } else { + $response = $httpRequest->get($resolvedUrl)->json(); + } $this->update([ 'data_payload' => $response, @@ -183,12 +183,14 @@ class Plugin extends Model try { // Make the request based on the verb - $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl); - - $response = $this->parseResponse($httpResponse); + if ($this->polling_verb === 'post') { + $response = $httpRequest->post($resolvedUrl)->json(); + } else { + $response = $httpRequest->get($resolvedUrl)->json(); + } // Check if response is an array at root level - if (array_keys($response) === range(0, count($response) - 1)) { + if (is_array($response) && array_keys($response) === range(0, count($response) - 1)) { // Response is a sequential array, nest under .data $combinedResponse["IDX_{$index}"] = ['data' => $response]; } else { @@ -209,56 +211,6 @@ class Plugin extends Model } } - /** - * Parse HTTP response, handling both JSON and XML content types - */ - private function parseResponse(Response $httpResponse): array - { - if ($httpResponse->header('Content-Type') && str_contains($httpResponse->header('Content-Type'), 'xml')) { - try { - // Convert XML to array and wrap under 'rss' key - $xml = simplexml_load_string($httpResponse->body()); - if ($xml === false) { - throw new Exception('Invalid XML content'); - } - - // Convert SimpleXML directly to array - $xmlArray = $this->xmlToArray($xml); - - return ['rss' => $xmlArray]; - } catch (Exception $e) { - Log::warning('Failed to parse XML response: '.$e->getMessage()); - - return ['error' => 'Failed to parse XML response']; - } - } - - // Default to JSON parsing - try { - return $httpResponse->json() ?? []; - } catch (Exception $e) { - Log::warning('Failed to parse JSON response: '.$e->getMessage()); - - return ['error' => 'Failed to parse JSON response']; - } - } - - /** - * Convert SimpleXML object to array recursively - */ - private function xmlToArray(SimpleXMLElement $xml): array - { - $array = (array) $xml; - - foreach ($array as $key => $value) { - if ($value instanceof SimpleXMLElement) { - $array[$key] = $this->xmlToArray($value); - } - } - - return $array; - } - /** * Apply Liquid template replacements (converts 'with' syntax to comma syntax) */ diff --git a/composer.json b/composer.json index 0d3fc42..8f3079d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "require": { "php": "^8.2", "ext-imagick": "*", - "ext-simplexml": "*", "ext-zip": "*", "bnussbau/laravel-trmnl-blade": "2.0.*", "bnussbau/trmnl-pipeline-php": "^0.3.0", diff --git a/composer.lock b/composer.lock index 44c00c7..97a6c15 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.357.0", + "version": "3.356.43", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "0325c653b022aedb38f397cf559ab4d0f7967901" + "reference": "5722f6ea95240ef28f1ded35448bedcccb0b0883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0325c653b022aedb38f397cf559ab4d0f7967901", - "reference": "0325c653b022aedb38f397cf559ab4d0f7967901", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5722f6ea95240ef28f1ded35448bedcccb0b0883", + "reference": "5722f6ea95240ef28f1ded35448bedcccb0b0883", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.357.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.356.43" }, - "time": "2025-10-22T19:43:07+00:00" + "time": "2025-10-21T19:13:44+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -1618,16 +1618,16 @@ }, { "name": "laravel/framework", - "version": "v12.35.1", + "version": "v12.35.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d6d6e3cb68238e2fb25b440f222442adef5a8a15" + "reference": "9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d6d6e3cb68238e2fb25b440f222442adef5a8a15", - "reference": "d6d6e3cb68238e2fb25b440f222442adef5a8a15", + "url": "https://api.github.com/repos/laravel/framework/zipball/9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef", + "reference": "9583ef9e405a71d5b8c04ff6efd05a7ef9a5baef", "shasum": "" }, "require": { @@ -1833,7 +1833,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-23T15:25:03+00:00" + "time": "2025-10-21T15:15:41+00:00" }, { "name": "laravel/prompts", @@ -10471,16 +10471,16 @@ }, { "name": "rector/rector", - "version": "2.2.5", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "fb9418af7777dfb1c87a536dc58398b5b07c74b9" + "reference": "904f12f23858ef54ec5782b05cb2979b703cb185" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/fb9418af7777dfb1c87a536dc58398b5b07c74b9", - "reference": "fb9418af7777dfb1c87a536dc58398b5b07c74b9", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/904f12f23858ef54ec5782b05cb2979b703cb185", + "reference": "904f12f23858ef54ec5782b05cb2979b703cb185", "shasum": "" }, "require": { @@ -10519,7 +10519,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.5" + "source": "https://github.com/rectorphp/rector/tree/2.2.4" }, "funding": [ { @@ -10527,7 +10527,7 @@ "type": "github" } ], - "time": "2025-10-23T11:22:37+00:00" + "time": "2025-10-22T07:50:23+00:00" }, { "name": "sebastian/cli-parser", diff --git a/tests/Feature/PluginXmlResponseTest.php b/tests/Feature/PluginXmlResponseTest.php deleted file mode 100644 index 308d914..0000000 --- a/tests/Feature/PluginXmlResponseTest.php +++ /dev/null @@ -1,171 +0,0 @@ - Http::response([ - 'title' => 'Test Data', - 'items' => [ - ['id' => 1, 'name' => 'Item 1'], - ['id' => 2, 'name' => 'Item 2'], - ], - ], 200, ['Content-Type' => 'application/json']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => 'https://example.com/api/data', - 'polling_verb' => 'get', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toBe([ - 'title' => 'Test Data', - 'items' => [ - ['id' => 1, 'name' => 'Item 1'], - ['id' => 2, 'name' => 'Item 2'], - ], - ]); -}); - -test('plugin parses XML responses and wraps under rss key', function (): void { - $xmlContent = ' - - - Test RSS Feed - - Test Item 1 - Description 1 - - - Test Item 2 - Description 2 - - - '; - - Http::fake([ - 'example.com/feed.xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => 'https://example.com/feed.xml', - 'polling_verb' => 'get', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toHaveKey('rss'); - expect($plugin->data_payload['rss'])->toHaveKey('@attributes'); - expect($plugin->data_payload['rss'])->toHaveKey('channel'); - expect($plugin->data_payload['rss']['channel']['title'])->toBe('Test RSS Feed'); - expect($plugin->data_payload['rss']['channel']['item'])->toHaveCount(2); -}); - -test('plugin handles non-XML content-type as JSON', function (): void { - $jsonContent = '{"title": "Test Data", "items": [1, 2, 3]}'; - - Http::fake([ - 'example.com/data' => Http::response($jsonContent, 200, ['Content-Type' => 'text/plain']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => 'https://example.com/data', - 'polling_verb' => 'get', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toBe([ - 'title' => 'Test Data', - 'items' => [1, 2, 3], - ]); -}); - -test('plugin handles invalid XML gracefully', function (): void { - $invalidXml = 'unclosed tag'; - - Http::fake([ - 'example.com/invalid.xml' => Http::response($invalidXml, 200, ['Content-Type' => 'application/xml']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => 'https://example.com/invalid.xml', - 'polling_verb' => 'get', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toBe(['error' => 'Failed to parse XML response']); -}); - -test('plugin handles multiple URLs with mixed content types', function (): void { - $jsonResponse = ['title' => 'JSON Data', 'items' => [1, 2, 3]]; - $xmlContent = 'XML Data'; - - Http::fake([ - 'example.com/json' => Http::response($jsonResponse, 200, ['Content-Type' => 'application/json']), - 'example.com/xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => "https://example.com/json\nhttps://example.com/xml", - 'polling_verb' => 'get', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toHaveKey('IDX_0'); - expect($plugin->data_payload)->toHaveKey('IDX_1'); - - // First URL should be JSON - expect($plugin->data_payload['IDX_0'])->toBe($jsonResponse); - - // Second URL should be XML wrapped under rss - expect($plugin->data_payload['IDX_1'])->toHaveKey('rss'); - expect($plugin->data_payload['IDX_1']['rss']['item'])->toBe('XML Data'); -}); - -test('plugin handles POST requests with XML responses', function (): void { - $xmlContent = 'successtest'; - - Http::fake([ - 'example.com/api' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']), - ]); - - $plugin = Plugin::factory()->create([ - 'data_strategy' => 'polling', - 'polling_url' => 'https://example.com/api', - 'polling_verb' => 'post', - 'polling_body' => '{"query": "test"}', - ]); - - $plugin->updateDataPayload(); - - $plugin->refresh(); - - expect($plugin->data_payload)->toHaveKey('rss'); - expect($plugin->data_payload['rss'])->toHaveKey('status'); - expect($plugin->data_payload['rss'])->toHaveKey('data'); - expect($plugin->data_payload['rss']['status'])->toBe('success'); - expect($plugin->data_payload['rss']['data'])->toBe('test'); -});