feat(ccli): serve CCLI bookmarklet JS

This commit is contained in:
Thorsten Bus 2026-05-10 19:34:30 +02:00
parent cd44d6289c
commit cd0a72124d
3 changed files with 91 additions and 0 deletions

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Config;
final class BookmarkletController extends Controller
{
public function show(): Response
{
$appUrl = rtrim((string) Config::get('app.url', ''), '/');
$bookmarkletScript = <<<'BOOKMARKLET'
(function(){
var APP_URL = '__APP_URL__';
if(!location.hostname.includes('songselect.ccli.com')){
alert('Bitte öffne dieses Lesezeichen auf einer SongSelect Liedseite (songselect.ccli.com).');
return;
}
var title = (document.querySelector('h1, .song-title, [class*="title"]') || {}).innerText || document.title || '';
var author = (document.querySelector('.song-authors, .song-artist, [class*="author"]') || {}).innerText || '';
var bodyText = document.body ? document.body.innerText : '';
var ccliMatch = bodyText.match(/CCLI[\s#-]*(\d+)/i);
var ccliId = ccliMatch ? ccliMatch[1] : '';
var payload = {
title: title.trim(),
author: author.trim(),
ccliId: ccliId,
sourceUrl: location.href,
rawText: bodyText
};
var encoded = btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
window.open(APP_URL + '/songs/import-from-ccli-paste?prefill=' + encoded, '_blank');
})();
BOOKMARKLET;
$bookmarkletScript = str_replace('__APP_URL__', $appUrl, $bookmarkletScript);
$singleLine = 'javascript:'.preg_replace('/\s+/', ' ', $bookmarkletScript);
return response($singleLine, 200, [
'Content-Type' => 'text/javascript; charset=utf-8',
'Cache-Control' => 'public, max-age=3600',
]);
}
}

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\ApiLogController; use App\Http\Controllers\ApiLogController;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\BookmarkletController;
use App\Http\Controllers\LabelImportController; use App\Http\Controllers\LabelImportController;
use App\Http\Controllers\MacroAssignmentController; use App\Http\Controllers\MacroAssignmentController;
use App\Http\Controllers\MacroImportController; use App\Http\Controllers\MacroImportController;
@ -46,6 +47,8 @@
->middleware('auth') ->middleware('auth')
->name('logout'); ->name('logout');
Route::get('/bookmarklets/ccli-import.js', [BookmarkletController::class, 'show'])->name('bookmarklets.ccli');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('/', function () { Route::get('/', function () {
return redirect()->route('dashboard'); return redirect()->route('dashboard');

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('bookmarklet endpoint returns 200 with text/javascript content type', function () {
$response = $this->get('/bookmarklets/ccli-import.js');
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'text/javascript; charset=utf-8');
});
test('bookmarklet response starts with javascript: prefix', function () {
$response = $this->get('/bookmarklets/ccli-import.js');
expect($response->getContent())->toStartWith('javascript:');
});
test('bookmarklet response is a single line with no actual newlines', function () {
$response = $this->get('/bookmarklets/ccli-import.js');
$content = $response->getContent();
expect(substr_count($content, "\n"))->toBe(0);
});
test('bookmarklet response contains app URL and import path', function () {
$response = $this->get('/bookmarklets/ccli-import.js');
$content = $response->getContent();
expect($content)->toContain('import-from-ccli-paste');
expect($content)->toContain('songselect.ccli.com');
expect($content)->toContain('btoa');
});
test('bookmarklet endpoint does not require authentication', function () {
$response = $this->get('/bookmarklets/ccli-import.js');
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'text/javascript; charset=utf-8');
});