v1.0.1
This commit is contained in:
parent
1b803f077b
commit
91fd6c2f43
12
README.md
12
README.md
|
|
@ -1,11 +1,13 @@
|
|||
# Calendar plugin for wordpress site
|
||||
|
||||
Status as of 2026-03-31 when codex credit ran out:
|
||||
1. Removal of plugin did not work. Need to set all files owner to www-data.
|
||||
|
||||
2. Deletion of event in ui doesn't delete event in thunderbird
|
||||
3. Cannot subscribe to an empty calendar in thunderbird
|
||||
4. Click inside month cell doesn't add event.
|
||||
5. Login pane hidden under website hero/banner image
|
||||
1. Removal of plugin did not work. Need to set all files owner to www-data. - done
|
||||
2. Deletion of event in ui doesn't delete event in thunderbird - done
|
||||
3. Cannot subscribe to an empty calendar in thunderbird - ok
|
||||
4. Click inside month cell doesn't add event. - cannot reproduce
|
||||
5. Login pane hidden under website hero/banner image - done
|
||||
6. Updating an events description via caldav creates weird sequences, e.g.
|
||||
a space ends up as: text/html,%C2%A0":
|
||||
7. In a private window, Click on event from not-logged-in calendar page shows event as a dialog box, it should show it as a panel with a subset of the edit event panel - i.e. the title, category, location, start and end and description. -
|
||||
8.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: Calendar Plugin
|
||||
* Plugin URI: https://chezstephens.org.uk
|
||||
* Description: Provides a single shared calendar for WordPress with public display, authenticated event editing, user approval workflow, ICS publishing, and CalDAV read/write sync. Supports recurring events, single-occurrence exceptions, admin setup and diagnostics pages, and shortcode rendering for full calendar and upcoming-events sidebar views.
|
||||
* Version: 0.1.15
|
||||
* Version: 1.0.1
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.1
|
||||
* Author: Adrian Stephens (with AI assistance)
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ final class IcsService
|
|||
if (!$in) {
|
||||
continue;
|
||||
}
|
||||
[$left, $value] = array_pad(explode(':', $line, 2), 2, '');
|
||||
[$left, $value] = $this->splitContentLine($line);
|
||||
if ($left === '') {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -364,6 +364,23 @@ final class IcsService
|
|||
return $in ? $props : null;
|
||||
}
|
||||
|
||||
private function splitContentLine(string $line): array
|
||||
{
|
||||
$inQuotes = false;
|
||||
$len = strlen($line);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$ch = $line[$i];
|
||||
if ($ch === '"') {
|
||||
$inQuotes = !$inQuotes;
|
||||
continue;
|
||||
}
|
||||
if ($ch === ':' && !$inQuotes) {
|
||||
return [substr($line, 0, $i), substr($line, $i + 1)];
|
||||
}
|
||||
}
|
||||
return [$line, ''];
|
||||
}
|
||||
|
||||
private function parseIcsDateTime(string $value, bool $dateOnly): ?string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
|
|
|||
|
|
@ -180,10 +180,10 @@ final class Plugin
|
|||
<p id="cp-status" style="margin:10px 0 8px 0;"></p>
|
||||
<h3 id="cp-events-title" style="margin:0 0 8px 0;">Events</h3>
|
||||
<div id="cp-view-panel"></div>
|
||||
<ul id="cp-public-list" style="margin-top:8px;"></ul>
|
||||
<ul id="cp-public-list" style="margin-top:8px;list-style:none;padding-left:0;"></ul>
|
||||
|
||||
<div id="cp-auth-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div style="max-width:560px;margin:8vh auto;background:#fff;border-radius:8px;padding:12px;">
|
||||
<div id="cp-auth-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;align-items:center;justify-content:center;padding:12px;box-sizing:border-box;">
|
||||
<div style="width:min(560px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:8px;padding:12px;">
|
||||
<h3 style="margin:0 0 8px 0;">Account Login</h3>
|
||||
<p id="cp-auth-status" style="margin:0 0 8px 0;"></p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||||
|
|
@ -203,7 +203,7 @@ final class Plugin
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cp-editor-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div id="cp-editor-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;">
|
||||
<div style="max-width:900px;margin:3vh auto;background:#fff;border-radius:8px;padding:12px;max-height:94vh;overflow:auto;">
|
||||
<h3 id="cp-editor-title" style="margin:0 0 8px 0;">Create Event</h3>
|
||||
<input id="cp-event-id" type="hidden" />
|
||||
|
|
@ -252,7 +252,7 @@ final class Plugin
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cp-details-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div id="cp-details-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;">
|
||||
<div style="max-width:900px;margin:4vh auto;background:#fff;border-radius:8px;padding:12px;max-height:90vh;overflow:auto;">
|
||||
<h3 style="margin:0 0 8px 0;">Event Details</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;">
|
||||
|
|
@ -287,6 +287,35 @@ final class Plugin
|
|||
const esc=(v)=>{const d=document.createElement("div"); d.textContent=v==null?"":String(v); return d.innerHTML;};
|
||||
const localYmd=(d)=>{const p=(n)=>String(n).padStart(2,"0"); return d.getFullYear()+"-"+p(d.getMonth()+1)+"-"+p(d.getDate());};
|
||||
const dmy=(d)=>{const p=(n)=>String(n).padStart(2,"0"); return p(d.getDate())+"/"+p(d.getMonth()+1)+"/"+d.getFullYear();};
|
||||
const longDate=(iso)=>{
|
||||
if(!iso){return "";}
|
||||
const d=new Date(iso);
|
||||
if(Number.isNaN(d.getTime())){return ymd(iso);}
|
||||
return d.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"});
|
||||
};
|
||||
const timeValue=(iso)=>{
|
||||
if(!iso){return "";}
|
||||
const d=new Date(iso);
|
||||
if(Number.isNaN(d.getTime())){return "";}
|
||||
let h=d.getHours();
|
||||
const m=d.getMinutes();
|
||||
const mer=h>=12?"pm":"am";
|
||||
h=h%12;
|
||||
if(h===0){h=12;}
|
||||
if(m===0){return `${h}${mer}`;}
|
||||
return `${h}.${String(m).padStart(2,"0")}${mer}`;
|
||||
};
|
||||
const timeRange=(startIso,endIso)=>{
|
||||
const s=new Date(startIso);
|
||||
const e=new Date(endIso);
|
||||
if(Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())){return "";}
|
||||
const sm=s.getHours()>=12?"pm":"am";
|
||||
const em=e.getHours()>=12?"pm":"am";
|
||||
let sv=timeValue(startIso);
|
||||
const ev=timeValue(endIso);
|
||||
if(sm===em){sv=sv.replace(/(am|pm)$/,"");}
|
||||
return `${sv}–${ev}`;
|
||||
};
|
||||
const itemId=(it)=>String(it.event_id||it.id||"");
|
||||
const futureWrap=()=>s("cp-future-wrap");
|
||||
const monthNames=["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||
|
|
@ -632,23 +661,23 @@ final class Plugin
|
|||
|
||||
const rowsForDay=(items)=>{
|
||||
const t=theme();
|
||||
return items.map(it=>`<div data-id="${esc(itemId(it))}" style="cursor:pointer;margin:2px 0;padding:3px 4px;border:1px solid ${t.border};background:${t.cellBg};border-left:3px solid ${t.accent};border-radius:4px;color:${t.text};">${esc(it.all_day_event?"All-day":hm(it.occurrence_start))} ${esc(it.title||"")}</div>`).join("");
|
||||
return items.map(it=>`<div data-cp-id="${esc(itemId(it))}" style="cursor:pointer;margin:2px 0;padding:3px 4px;border:1px solid ${t.border};background:${t.cellBg};border-left:3px solid ${t.accent};border-radius:4px;color:${t.text};">${esc(it.all_day_event?"All-day":hm(it.occurrence_start))} ${esc(it.title||"")}</div>`).join("");
|
||||
};
|
||||
|
||||
const bindClicks=()=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-id], #cp-public-list [data-id]")).forEach(el=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-cp-id], #cp-public-list [data-cp-id]")).forEach(el=>{
|
||||
el.addEventListener("click",async()=>{
|
||||
const id=el.getAttribute("data-id")||"";
|
||||
const id=el.getAttribute("data-cp-id")||"";
|
||||
const it=lastItems.find(x=>String(itemId(x))===String(id))||null;
|
||||
if(it){await openEditorForItem(it);}
|
||||
});
|
||||
});
|
||||
};
|
||||
const bindCreateClicks=()=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-create-date]")).forEach(el=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-cp-create-date]")).forEach(el=>{
|
||||
el.addEventListener("click",(ev)=>{
|
||||
if(ev.target && ev.target.closest("[data-id]")){return;}
|
||||
const dateYmd=el.getAttribute("data-create-date")||"";
|
||||
if(ev.target && ev.target.closest("[data-cp-id]")){return;}
|
||||
const dateYmd=el.getAttribute("data-cp-create-date")||"";
|
||||
if(dateYmd){openEditorCreateAt(dateYmd);}
|
||||
});
|
||||
});
|
||||
|
|
@ -668,14 +697,19 @@ final class Plugin
|
|||
panel.innerHTML="";
|
||||
(items||[]).slice(0,300).forEach(it=>{
|
||||
const li=document.createElement("li");
|
||||
li.setAttribute("data-id", itemId(it));
|
||||
li.setAttribute("data-cp-id", itemId(it));
|
||||
li.style.cursor="pointer";
|
||||
li.style.background=t.cellBg;
|
||||
li.style.border=`1px solid ${t.border}`;
|
||||
li.style.margin="4px 0";
|
||||
li.style.padding="6px 8px";
|
||||
li.style.borderRadius="4px";
|
||||
li.textContent=ymd(it.occurrence_start)+" "+(it.all_day_event?"All-day":(hm(it.occurrence_start)+"–"+hm(it.occurrence_end)))+" "+(it.description||it.title||"");
|
||||
const dateLabel=longDate(it.occurrence_start||it.start_datetime);
|
||||
const timeLabel=it.all_day_event?"All day":timeRange(it.occurrence_start,it.occurrence_end);
|
||||
const title=it.title||"";
|
||||
const desc=String(it.description||"").trim();
|
||||
const headline=[dateLabel,timeLabel,title].filter(Boolean).join(", ");
|
||||
li.innerHTML=`<div>${esc(headline)}</div>${(desc!=="" && desc!==title)?`<div style="color:${t.mutedText};margin-top:2px;">${esc(desc)}</div>`:""}`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
bindClicks();
|
||||
|
|
@ -708,7 +742,7 @@ final class Plugin
|
|||
const d=new Date(start); d.setDate(start.getDate()+i);
|
||||
const k=localYmd(d);
|
||||
const ev=(map[k]||[]).sort((a,b)=>String(a.occurrence_start).localeCompare(String(b.occurrence_start)));
|
||||
cells+=`<td data-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${esc(k)}</strong></div>${rowsForDay(ev)}</td>`;
|
||||
cells+=`<td data-cp-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${esc(k)}</strong></div>${rowsForDay(ev)}</td>`;
|
||||
}
|
||||
panel.innerHTML=`<table style="width:100%;border-collapse:collapse;"><tr><th style="width:65px;border:1px solid ${t.border};background:${t.headBg};">Time</th>${dow.map(n=>`<th style="border:1px solid ${t.border};background:${t.headBg};">${n}</th>`).join("")}</tr><tr><td style="border:1px solid ${t.border};vertical-align:top;padding:4px;background:${t.mutedBg};color:${t.mutedText};">00:00<br>06:00<br>12:00<br>18:00</td>${cells}</tr></table>`;
|
||||
bindClicks();
|
||||
|
|
@ -732,7 +766,7 @@ final class Plugin
|
|||
const ev=(map[k]||[]).sort((a,b)=>String(a.occurrence_start).localeCompare(String(b.occurrence_start)));
|
||||
const inMonth=d.getMonth()===anchor.getMonth();
|
||||
if(inMonth){rowHasInMonth=true;}
|
||||
tds+=`<td data-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;min-height:80px;opacity:${inMonth?1:0.55};background:${inMonth?t.cellBg:t.mutedBg};"><div style="color:${t.mutedText};"><strong>${esc(k.slice(8,10))}</strong></div>${rowsForDay(ev.slice(0,6))}</td>`;
|
||||
tds+=`<td data-cp-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;min-height:80px;opacity:${inMonth?1:0.55};background:${inMonth?t.cellBg:t.mutedBg};"><div style="color:${t.mutedText};"><strong>${esc(k.slice(8,10))}</strong></div>${rowsForDay(ev.slice(0,6))}</td>`;
|
||||
}
|
||||
rowParts.push({html:`<tr>${tds}</tr>`,has:rowHasInMonth});
|
||||
}
|
||||
|
|
@ -757,15 +791,15 @@ final class Plugin
|
|||
const d=new Date(start); d.setDate(start.getDate()+i);
|
||||
const k=localYmd(d);
|
||||
const same=d.getMonth()===m;
|
||||
g+=`<div data-day="${k}" style="cursor:pointer;padding:2px;border:1px solid ${t.border};text-align:center;opacity:${same?1:0.35};font-weight:${has(k)?700:400};background:${same?t.cellBg:t.mutedBg};">${d.getDate()}</div>`;
|
||||
g+=`<div data-cp-day="${k}" style="cursor:pointer;padding:2px;border:1px solid ${t.border};text-align:center;opacity:${same?1:0.35};font-weight:${has(k)?700:400};background:${same?t.cellBg:t.mutedBg};">${d.getDate()}</div>`;
|
||||
}
|
||||
out+=`<div style="border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${mon[m]}</strong></div><div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px;">${g}</div></div>`;
|
||||
}
|
||||
out+='</div>';
|
||||
panel.innerHTML=out;
|
||||
Array.from(panel.querySelectorAll("[data-day]")).forEach(el=>{
|
||||
Array.from(panel.querySelectorAll("[data-cp-day]")).forEach(el=>{
|
||||
el.addEventListener("click",()=>{
|
||||
s("cp-date").value=el.getAttribute("data-day")||s("cp-date").value;
|
||||
s("cp-date").value=el.getAttribute("data-cp-day")||s("cp-date").value;
|
||||
s("cp-view").value="week";
|
||||
loadPublic();
|
||||
});
|
||||
|
|
@ -785,7 +819,7 @@ final class Plugin
|
|||
futureWrap().style.display=s("cp-view").value==="list"?"flex":"none";
|
||||
};
|
||||
|
||||
s("cp-open-login-btn").onclick=()=>{setAuthStatus("",false); s("cp-auth-modal").style.display="block";};
|
||||
s("cp-open-login-btn").onclick=()=>{setAuthStatus("",false); s("cp-auth-modal").style.display="flex";};
|
||||
s("cp-close-login-btn").onclick=()=>{s("cp-auth-modal").style.display="none";};
|
||||
s("cp-logout-btn").onclick=async()=>{
|
||||
await api("/users/logout",{method:"POST"});
|
||||
|
|
@ -932,7 +966,7 @@ final class Plugin
|
|||
const tokenFromUrl=(new URLSearchParams(window.location.search)).get("calendar_verify_token");
|
||||
if(tokenFromUrl){
|
||||
s("cp-verify-token").value=tokenFromUrl;
|
||||
s("cp-auth-modal").style.display="block";
|
||||
s("cp-auth-modal").style.display="flex";
|
||||
setAuthStatus("Verification token loaded from link. Press Verify Email.",false);
|
||||
}
|
||||
|
||||
|
|
@ -979,18 +1013,19 @@ HTML
|
|||
}
|
||||
|
||||
$rows = [];
|
||||
$sidebarTz = new \DateTimeZone('Europe/London');
|
||||
foreach ($items as $item) {
|
||||
$start = (string) ($item['occurrence_start'] ?? '');
|
||||
$end = (string) ($item['occurrence_end'] ?? '');
|
||||
$startTs = strtotime($start);
|
||||
$endTs = strtotime($end);
|
||||
$dateLabel = $startTs !== false ? date('j F Y', $startTs) : substr($start, 0, 10);
|
||||
$startDt = $this->toSidebarDateTime($start, $sidebarTz);
|
||||
$endDt = $this->toSidebarDateTime($end, $sidebarTz);
|
||||
$dateLabel = $startDt !== null ? $startDt->format('j F Y') : substr($start, 0, 10);
|
||||
$timeLabel = '';
|
||||
if ($startTs !== false && $endTs !== false) {
|
||||
$startTime = date('H:i', $startTs);
|
||||
$endTime = date('H:i', $endTs);
|
||||
if ($startDt !== null && $endDt !== null) {
|
||||
$startTime = $startDt->format('H:i');
|
||||
$endTime = $endDt->format('H:i');
|
||||
if ($startTime !== '00:00' || $endTime !== '00:00') {
|
||||
$timeLabel = $this->formatSidebarTimeRange($startTs, $endTs);
|
||||
$timeLabel = $this->formatSidebarTimeRange($startDt, $endDt);
|
||||
}
|
||||
}
|
||||
$title = trim((string) ($item['title'] ?? ''));
|
||||
|
|
@ -1010,12 +1045,24 @@ HTML
|
|||
return '<div class="calendar-plugin-shell" data-mode="sidebar">' . implode('', $rows) . '</div>';
|
||||
}
|
||||
|
||||
private function formatSidebarTimeRange(int $startTs, int $endTs): string
|
||||
private function toSidebarDateTime(string $value, \DateTimeZone $timezone): ?\DateTimeImmutable
|
||||
{
|
||||
$startMeridiem = strtolower(date('a', $startTs));
|
||||
$endMeridiem = strtolower(date('a', $endTs));
|
||||
$startLabel = $this->formatSidebarTimeValue($startTs);
|
||||
$endLabel = $this->formatSidebarTimeValue($endTs);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (new \DateTimeImmutable($value))->setTimezone($timezone);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function formatSidebarTimeRange(\DateTimeImmutable $startDt, \DateTimeImmutable $endDt): string
|
||||
{
|
||||
$startMeridiem = strtolower($startDt->format('a'));
|
||||
$endMeridiem = strtolower($endDt->format('a'));
|
||||
$startLabel = $this->formatSidebarTimeValue($startDt);
|
||||
$endLabel = $this->formatSidebarTimeValue($endDt);
|
||||
if ($startMeridiem === $endMeridiem) {
|
||||
$startLabel = preg_replace('/(am|pm)$/', '', $startLabel) ?: $startLabel;
|
||||
return $startLabel . '–' . $endLabel;
|
||||
|
|
@ -1023,11 +1070,11 @@ HTML
|
|||
return $startLabel . '–' . $endLabel;
|
||||
}
|
||||
|
||||
private function formatSidebarTimeValue(int $ts): string
|
||||
private function formatSidebarTimeValue(\DateTimeImmutable $dt): string
|
||||
{
|
||||
$hour = (int) date('G', $ts);
|
||||
$minute = (int) date('i', $ts);
|
||||
$meridiem = strtolower(date('a', $ts));
|
||||
$hour = (int) $dt->format('G');
|
||||
$minute = (int) $dt->format('i');
|
||||
$meridiem = strtolower($dt->format('a'));
|
||||
$hour12 = $hour % 12;
|
||||
if ($hour12 === 0) {
|
||||
$hour12 = 12;
|
||||
|
|
@ -1375,7 +1422,7 @@ HTML
|
|||
return [
|
||||
'status' => 'ok',
|
||||
'plugin' => 'calendar-plugin',
|
||||
'version' => '0.1.15',
|
||||
'version' => '1.0.1',
|
||||
'db_prefix' => $this->db->getPrefix(),
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastr
|
|||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
b0e95460e705b9059bf2f197c17f17c51c9bd5492ca69eaa073ebbb808542fe0 ./src/Plugin.php
|
||||
7c740beff8c22271e1d3e578368c57b9d4b9ebdc42852f0fc967441d39c87d7b ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
af5fe54d7e0ded93bddd52890eb2d7f78ae378e3c8008dafabb527d152336f5d ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
f48006beb0c5c8d7a98e9f33f80a6a08fb92a999c28c937f9afd814e98de0a05 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
8af4c2cb42b8ec24b0cce507d9e0911063c8a4caa5023216125fdc5988bc5eaa ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
9557a73776dcd4d43288d6415589555eaa537eee22d13592678699bc4cd02efc ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
f48006beb0c5c8d7a98e9f33f80a6a08fb92a999c28c937f9afd814e98de0a05 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
83e082e7afd6504c89241a11a2fcfdbd685985083a932fa30bbfc2df7518c3c7 ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
a3e0e5bbbf679c4f2f98de78149fa56365f29f4b170b8d2d7e8cafa98a730e43 ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
f48006beb0c5c8d7a98e9f33f80a6a08fb92a999c28c937f9afd814e98de0a05 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
db3af8bbd3c7bb7a84e2309ea5ba5c350cb02c72ac4849778d6c219686c36d8f ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
c1a836ec784d11ede886a399b128ed803c0fbaec60458e1fb6b72f6223745ee8 ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
37b8f3077dcf9c0eb40dbcfefa7a5d8cebfe616051d827ad6ae3bb5a5fbccec7 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
3f3dd111db8a73fc2dfb8e8ec23c04cc15b805d0e9fe3006a63f4e656a80ff1d ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
67c918ffb3c537d09017ee1c0650d4007ec376f2b75632bb366e5f370b3671a3 ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
37b8f3077dcf9c0eb40dbcfefa7a5d8cebfe616051d827ad6ae3bb5a5fbccec7 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
598ef126274bf5b19bf47e18c617f26dfa95639f73602d5113e3a50ef12359c1 ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
93108c80fea01a4d1de36fbf3b02148c060051ef4c6ac85e34076061e9b222ac ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
37b8f3077dcf9c0eb40dbcfefa7a5d8cebfe616051d827ad6ae3bb5a5fbccec7 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
b9d407b4415d3fd1db145b9ba0b0f950e2993a72828783caf94d0d537fe077ff ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
|||
042b7c45db8136ba535ea1d892a02826fb75a8607fc7f07984b9f7692dc47040 ./calendar-plugin.php
|
||||
1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb ./src/Contracts/AuthAdapterInterface.php
|
||||
25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d ./src/Contracts/DatabaseAdapterInterface.php
|
||||
4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba ./src/Contracts/HttpAdapterInterface.php
|
||||
15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 ./src/Contracts/OptionsAdapterInterface.php
|
||||
da068811cd923bd6cf04a209b9b4eec55fa57aeedfccaa3250ebd37b906c6d04 ./src/Domain/CalDavService.php
|
||||
c8ab00f23e7cd198228734515c3ce9a2589b1ab6cb815307f3b8f7a3051e04d2 ./src/Domain/EventService.php
|
||||
37b8f3077dcf9c0eb40dbcfefa7a5d8cebfe616051d827ad6ae3bb5a5fbccec7 ./src/Domain/IcsService.php
|
||||
fd5377f7852b0f35550a453451d98882b8488b332d9a051dac603851c7b586d1 ./src/Domain/RecurrenceExpander.php
|
||||
20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 ./src/Domain/SettingsService.php
|
||||
9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f ./src/Domain/UserService.php
|
||||
5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b ./src/Infrastructure/ServiceContainer.php
|
||||
e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastructure/WordPress/MigrationManager.php
|
||||
8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 ./src/Infrastructure/WordPress/WordPressAuthAdapter.php
|
||||
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
|
||||
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
|
||||
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
|
||||
d268d1b18384c399bd5cd5d31f96b26d271c559d656ef447428594f76078ff46 ./src/Plugin.php
|
||||
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
|
||||
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
|
||||
Binary file not shown.
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: Calendar Plugin
|
||||
* Plugin URI: https://chezstephens.org.uk
|
||||
* Description: Provides a single shared calendar for WordPress with public display, authenticated event editing, user approval workflow, ICS publishing, and CalDAV read/write sync. Supports recurring events, single-occurrence exceptions, admin setup and diagnostics pages, and shortcode rendering for full calendar and upcoming-events sidebar views.
|
||||
* Version: 0.1.15
|
||||
* Version: 1.0.1
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.1
|
||||
* Author: Adrian Stephens (with AI assistance)
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ final class IcsService
|
|||
if (!$in) {
|
||||
continue;
|
||||
}
|
||||
[$left, $value] = array_pad(explode(':', $line, 2), 2, '');
|
||||
[$left, $value] = $this->splitContentLine($line);
|
||||
if ($left === '') {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -364,6 +364,23 @@ final class IcsService
|
|||
return $in ? $props : null;
|
||||
}
|
||||
|
||||
private function splitContentLine(string $line): array
|
||||
{
|
||||
$inQuotes = false;
|
||||
$len = strlen($line);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$ch = $line[$i];
|
||||
if ($ch === '"') {
|
||||
$inQuotes = !$inQuotes;
|
||||
continue;
|
||||
}
|
||||
if ($ch === ':' && !$inQuotes) {
|
||||
return [substr($line, 0, $i), substr($line, $i + 1)];
|
||||
}
|
||||
}
|
||||
return [$line, ''];
|
||||
}
|
||||
|
||||
private function parseIcsDateTime(string $value, bool $dateOnly): ?string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
|
|
|||
|
|
@ -180,10 +180,10 @@ final class Plugin
|
|||
<p id="cp-status" style="margin:10px 0 8px 0;"></p>
|
||||
<h3 id="cp-events-title" style="margin:0 0 8px 0;">Events</h3>
|
||||
<div id="cp-view-panel"></div>
|
||||
<ul id="cp-public-list" style="margin-top:8px;"></ul>
|
||||
<ul id="cp-public-list" style="margin-top:8px;list-style:none;padding-left:0;"></ul>
|
||||
|
||||
<div id="cp-auth-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div style="max-width:560px;margin:8vh auto;background:#fff;border-radius:8px;padding:12px;">
|
||||
<div id="cp-auth-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;align-items:center;justify-content:center;padding:12px;box-sizing:border-box;">
|
||||
<div style="width:min(560px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:8px;padding:12px;">
|
||||
<h3 style="margin:0 0 8px 0;">Account Login</h3>
|
||||
<p id="cp-auth-status" style="margin:0 0 8px 0;"></p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||||
|
|
@ -203,7 +203,7 @@ final class Plugin
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cp-editor-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div id="cp-editor-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;">
|
||||
<div style="max-width:900px;margin:3vh auto;background:#fff;border-radius:8px;padding:12px;max-height:94vh;overflow:auto;">
|
||||
<h3 id="cp-editor-title" style="margin:0 0 8px 0;">Create Event</h3>
|
||||
<input id="cp-event-id" type="hidden" />
|
||||
|
|
@ -252,7 +252,7 @@ final class Plugin
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cp-details-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:1000;">
|
||||
<div id="cp-details-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:2147483000;">
|
||||
<div style="max-width:900px;margin:4vh auto;background:#fff;border-radius:8px;padding:12px;max-height:90vh;overflow:auto;">
|
||||
<h3 style="margin:0 0 8px 0;">Event Details</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;">
|
||||
|
|
@ -287,6 +287,35 @@ final class Plugin
|
|||
const esc=(v)=>{const d=document.createElement("div"); d.textContent=v==null?"":String(v); return d.innerHTML;};
|
||||
const localYmd=(d)=>{const p=(n)=>String(n).padStart(2,"0"); return d.getFullYear()+"-"+p(d.getMonth()+1)+"-"+p(d.getDate());};
|
||||
const dmy=(d)=>{const p=(n)=>String(n).padStart(2,"0"); return p(d.getDate())+"/"+p(d.getMonth()+1)+"/"+d.getFullYear();};
|
||||
const longDate=(iso)=>{
|
||||
if(!iso){return "";}
|
||||
const d=new Date(iso);
|
||||
if(Number.isNaN(d.getTime())){return ymd(iso);}
|
||||
return d.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"});
|
||||
};
|
||||
const timeValue=(iso)=>{
|
||||
if(!iso){return "";}
|
||||
const d=new Date(iso);
|
||||
if(Number.isNaN(d.getTime())){return "";}
|
||||
let h=d.getHours();
|
||||
const m=d.getMinutes();
|
||||
const mer=h>=12?"pm":"am";
|
||||
h=h%12;
|
||||
if(h===0){h=12;}
|
||||
if(m===0){return `${h}${mer}`;}
|
||||
return `${h}.${String(m).padStart(2,"0")}${mer}`;
|
||||
};
|
||||
const timeRange=(startIso,endIso)=>{
|
||||
const s=new Date(startIso);
|
||||
const e=new Date(endIso);
|
||||
if(Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())){return "";}
|
||||
const sm=s.getHours()>=12?"pm":"am";
|
||||
const em=e.getHours()>=12?"pm":"am";
|
||||
let sv=timeValue(startIso);
|
||||
const ev=timeValue(endIso);
|
||||
if(sm===em){sv=sv.replace(/(am|pm)$/,"");}
|
||||
return `${sv}–${ev}`;
|
||||
};
|
||||
const itemId=(it)=>String(it.event_id||it.id||"");
|
||||
const futureWrap=()=>s("cp-future-wrap");
|
||||
const monthNames=["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||
|
|
@ -632,23 +661,23 @@ final class Plugin
|
|||
|
||||
const rowsForDay=(items)=>{
|
||||
const t=theme();
|
||||
return items.map(it=>`<div data-id="${esc(itemId(it))}" style="cursor:pointer;margin:2px 0;padding:3px 4px;border:1px solid ${t.border};background:${t.cellBg};border-left:3px solid ${t.accent};border-radius:4px;color:${t.text};">${esc(it.all_day_event?"All-day":hm(it.occurrence_start))} ${esc(it.title||"")}</div>`).join("");
|
||||
return items.map(it=>`<div data-cp-id="${esc(itemId(it))}" style="cursor:pointer;margin:2px 0;padding:3px 4px;border:1px solid ${t.border};background:${t.cellBg};border-left:3px solid ${t.accent};border-radius:4px;color:${t.text};">${esc(it.all_day_event?"All-day":hm(it.occurrence_start))} ${esc(it.title||"")}</div>`).join("");
|
||||
};
|
||||
|
||||
const bindClicks=()=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-id], #cp-public-list [data-id]")).forEach(el=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-cp-id], #cp-public-list [data-cp-id]")).forEach(el=>{
|
||||
el.addEventListener("click",async()=>{
|
||||
const id=el.getAttribute("data-id")||"";
|
||||
const id=el.getAttribute("data-cp-id")||"";
|
||||
const it=lastItems.find(x=>String(itemId(x))===String(id))||null;
|
||||
if(it){await openEditorForItem(it);}
|
||||
});
|
||||
});
|
||||
};
|
||||
const bindCreateClicks=()=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-create-date]")).forEach(el=>{
|
||||
Array.from(document.querySelectorAll("#cp-view-panel [data-cp-create-date]")).forEach(el=>{
|
||||
el.addEventListener("click",(ev)=>{
|
||||
if(ev.target && ev.target.closest("[data-id]")){return;}
|
||||
const dateYmd=el.getAttribute("data-create-date")||"";
|
||||
if(ev.target && ev.target.closest("[data-cp-id]")){return;}
|
||||
const dateYmd=el.getAttribute("data-cp-create-date")||"";
|
||||
if(dateYmd){openEditorCreateAt(dateYmd);}
|
||||
});
|
||||
});
|
||||
|
|
@ -668,14 +697,19 @@ final class Plugin
|
|||
panel.innerHTML="";
|
||||
(items||[]).slice(0,300).forEach(it=>{
|
||||
const li=document.createElement("li");
|
||||
li.setAttribute("data-id", itemId(it));
|
||||
li.setAttribute("data-cp-id", itemId(it));
|
||||
li.style.cursor="pointer";
|
||||
li.style.background=t.cellBg;
|
||||
li.style.border=`1px solid ${t.border}`;
|
||||
li.style.margin="4px 0";
|
||||
li.style.padding="6px 8px";
|
||||
li.style.borderRadius="4px";
|
||||
li.textContent=ymd(it.occurrence_start)+" "+(it.all_day_event?"All-day":(hm(it.occurrence_start)+"–"+hm(it.occurrence_end)))+" "+(it.description||it.title||"");
|
||||
const dateLabel=longDate(it.occurrence_start||it.start_datetime);
|
||||
const timeLabel=it.all_day_event?"All day":timeRange(it.occurrence_start,it.occurrence_end);
|
||||
const title=it.title||"";
|
||||
const desc=String(it.description||"").trim();
|
||||
const headline=[dateLabel,timeLabel,title].filter(Boolean).join(", ");
|
||||
li.innerHTML=`<div>${esc(headline)}</div>${(desc!=="" && desc!==title)?`<div style="color:${t.mutedText};margin-top:2px;">${esc(desc)}</div>`:""}`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
bindClicks();
|
||||
|
|
@ -708,7 +742,7 @@ final class Plugin
|
|||
const d=new Date(start); d.setDate(start.getDate()+i);
|
||||
const k=localYmd(d);
|
||||
const ev=(map[k]||[]).sort((a,b)=>String(a.occurrence_start).localeCompare(String(b.occurrence_start)));
|
||||
cells+=`<td data-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${esc(k)}</strong></div>${rowsForDay(ev)}</td>`;
|
||||
cells+=`<td data-cp-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${esc(k)}</strong></div>${rowsForDay(ev)}</td>`;
|
||||
}
|
||||
panel.innerHTML=`<table style="width:100%;border-collapse:collapse;"><tr><th style="width:65px;border:1px solid ${t.border};background:${t.headBg};">Time</th>${dow.map(n=>`<th style="border:1px solid ${t.border};background:${t.headBg};">${n}</th>`).join("")}</tr><tr><td style="border:1px solid ${t.border};vertical-align:top;padding:4px;background:${t.mutedBg};color:${t.mutedText};">00:00<br>06:00<br>12:00<br>18:00</td>${cells}</tr></table>`;
|
||||
bindClicks();
|
||||
|
|
@ -732,7 +766,7 @@ final class Plugin
|
|||
const ev=(map[k]||[]).sort((a,b)=>String(a.occurrence_start).localeCompare(String(b.occurrence_start)));
|
||||
const inMonth=d.getMonth()===anchor.getMonth();
|
||||
if(inMonth){rowHasInMonth=true;}
|
||||
tds+=`<td data-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;min-height:80px;opacity:${inMonth?1:0.55};background:${inMonth?t.cellBg:t.mutedBg};"><div style="color:${t.mutedText};"><strong>${esc(k.slice(8,10))}</strong></div>${rowsForDay(ev.slice(0,6))}</td>`;
|
||||
tds+=`<td data-cp-create-date="${k}" style="cursor:pointer;vertical-align:top;border:1px solid ${t.border};padding:4px;min-height:80px;opacity:${inMonth?1:0.55};background:${inMonth?t.cellBg:t.mutedBg};"><div style="color:${t.mutedText};"><strong>${esc(k.slice(8,10))}</strong></div>${rowsForDay(ev.slice(0,6))}</td>`;
|
||||
}
|
||||
rowParts.push({html:`<tr>${tds}</tr>`,has:rowHasInMonth});
|
||||
}
|
||||
|
|
@ -757,15 +791,15 @@ final class Plugin
|
|||
const d=new Date(start); d.setDate(start.getDate()+i);
|
||||
const k=localYmd(d);
|
||||
const same=d.getMonth()===m;
|
||||
g+=`<div data-day="${k}" style="cursor:pointer;padding:2px;border:1px solid ${t.border};text-align:center;opacity:${same?1:0.35};font-weight:${has(k)?700:400};background:${same?t.cellBg:t.mutedBg};">${d.getDate()}</div>`;
|
||||
g+=`<div data-cp-day="${k}" style="cursor:pointer;padding:2px;border:1px solid ${t.border};text-align:center;opacity:${same?1:0.35};font-weight:${has(k)?700:400};background:${same?t.cellBg:t.mutedBg};">${d.getDate()}</div>`;
|
||||
}
|
||||
out+=`<div style="border:1px solid ${t.border};padding:4px;background:${t.cellBg};"><div style="color:${t.mutedText};"><strong>${mon[m]}</strong></div><div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px;">${g}</div></div>`;
|
||||
}
|
||||
out+='</div>';
|
||||
panel.innerHTML=out;
|
||||
Array.from(panel.querySelectorAll("[data-day]")).forEach(el=>{
|
||||
Array.from(panel.querySelectorAll("[data-cp-day]")).forEach(el=>{
|
||||
el.addEventListener("click",()=>{
|
||||
s("cp-date").value=el.getAttribute("data-day")||s("cp-date").value;
|
||||
s("cp-date").value=el.getAttribute("data-cp-day")||s("cp-date").value;
|
||||
s("cp-view").value="week";
|
||||
loadPublic();
|
||||
});
|
||||
|
|
@ -785,7 +819,7 @@ final class Plugin
|
|||
futureWrap().style.display=s("cp-view").value==="list"?"flex":"none";
|
||||
};
|
||||
|
||||
s("cp-open-login-btn").onclick=()=>{setAuthStatus("",false); s("cp-auth-modal").style.display="block";};
|
||||
s("cp-open-login-btn").onclick=()=>{setAuthStatus("",false); s("cp-auth-modal").style.display="flex";};
|
||||
s("cp-close-login-btn").onclick=()=>{s("cp-auth-modal").style.display="none";};
|
||||
s("cp-logout-btn").onclick=async()=>{
|
||||
await api("/users/logout",{method:"POST"});
|
||||
|
|
@ -932,7 +966,7 @@ final class Plugin
|
|||
const tokenFromUrl=(new URLSearchParams(window.location.search)).get("calendar_verify_token");
|
||||
if(tokenFromUrl){
|
||||
s("cp-verify-token").value=tokenFromUrl;
|
||||
s("cp-auth-modal").style.display="block";
|
||||
s("cp-auth-modal").style.display="flex";
|
||||
setAuthStatus("Verification token loaded from link. Press Verify Email.",false);
|
||||
}
|
||||
|
||||
|
|
@ -979,18 +1013,19 @@ HTML
|
|||
}
|
||||
|
||||
$rows = [];
|
||||
$sidebarTz = new \DateTimeZone('Europe/London');
|
||||
foreach ($items as $item) {
|
||||
$start = (string) ($item['occurrence_start'] ?? '');
|
||||
$end = (string) ($item['occurrence_end'] ?? '');
|
||||
$startTs = strtotime($start);
|
||||
$endTs = strtotime($end);
|
||||
$dateLabel = $startTs !== false ? date('j F Y', $startTs) : substr($start, 0, 10);
|
||||
$startDt = $this->toSidebarDateTime($start, $sidebarTz);
|
||||
$endDt = $this->toSidebarDateTime($end, $sidebarTz);
|
||||
$dateLabel = $startDt !== null ? $startDt->format('j F Y') : substr($start, 0, 10);
|
||||
$timeLabel = '';
|
||||
if ($startTs !== false && $endTs !== false) {
|
||||
$startTime = date('H:i', $startTs);
|
||||
$endTime = date('H:i', $endTs);
|
||||
if ($startDt !== null && $endDt !== null) {
|
||||
$startTime = $startDt->format('H:i');
|
||||
$endTime = $endDt->format('H:i');
|
||||
if ($startTime !== '00:00' || $endTime !== '00:00') {
|
||||
$timeLabel = $this->formatSidebarTimeRange($startTs, $endTs);
|
||||
$timeLabel = $this->formatSidebarTimeRange($startDt, $endDt);
|
||||
}
|
||||
}
|
||||
$title = trim((string) ($item['title'] ?? ''));
|
||||
|
|
@ -1010,12 +1045,24 @@ HTML
|
|||
return '<div class="calendar-plugin-shell" data-mode="sidebar">' . implode('', $rows) . '</div>';
|
||||
}
|
||||
|
||||
private function formatSidebarTimeRange(int $startTs, int $endTs): string
|
||||
private function toSidebarDateTime(string $value, \DateTimeZone $timezone): ?\DateTimeImmutable
|
||||
{
|
||||
$startMeridiem = strtolower(date('a', $startTs));
|
||||
$endMeridiem = strtolower(date('a', $endTs));
|
||||
$startLabel = $this->formatSidebarTimeValue($startTs);
|
||||
$endLabel = $this->formatSidebarTimeValue($endTs);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (new \DateTimeImmutable($value))->setTimezone($timezone);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function formatSidebarTimeRange(\DateTimeImmutable $startDt, \DateTimeImmutable $endDt): string
|
||||
{
|
||||
$startMeridiem = strtolower($startDt->format('a'));
|
||||
$endMeridiem = strtolower($endDt->format('a'));
|
||||
$startLabel = $this->formatSidebarTimeValue($startDt);
|
||||
$endLabel = $this->formatSidebarTimeValue($endDt);
|
||||
if ($startMeridiem === $endMeridiem) {
|
||||
$startLabel = preg_replace('/(am|pm)$/', '', $startLabel) ?: $startLabel;
|
||||
return $startLabel . '–' . $endLabel;
|
||||
|
|
@ -1023,11 +1070,11 @@ HTML
|
|||
return $startLabel . '–' . $endLabel;
|
||||
}
|
||||
|
||||
private function formatSidebarTimeValue(int $ts): string
|
||||
private function formatSidebarTimeValue(\DateTimeImmutable $dt): string
|
||||
{
|
||||
$hour = (int) date('G', $ts);
|
||||
$minute = (int) date('i', $ts);
|
||||
$meridiem = strtolower(date('a', $ts));
|
||||
$hour = (int) $dt->format('G');
|
||||
$minute = (int) $dt->format('i');
|
||||
$meridiem = strtolower($dt->format('a'));
|
||||
$hour12 = $hour % 12;
|
||||
if ($hour12 === 0) {
|
||||
$hour12 = 12;
|
||||
|
|
@ -1375,7 +1422,7 @@ HTML
|
|||
return [
|
||||
'status' => 'ok',
|
||||
'plugin' => 'calendar-plugin',
|
||||
'version' => '0.1.15',
|
||||
'version' => '1.0.1',
|
||||
'db_prefix' => $this->db->getPrefix(),
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ Minimum mapping expectations:
|
|||
- Last modification timestamp -> `DTSTAMP` (and `LAST-MODIFIED` when available)
|
||||
- Recurrence rules -> `RRULE`
|
||||
- Recurrence exceptions -> `EXDATE` and/or additional `VEVENT` with matching `UID` plus `RECURRENCE-ID`
|
||||
- Parsing must correctly handle quoted property parameters containing `:` (for example `DESCRIPTION;ALTREP="data:text/html,..."`) so field values are not polluted by parameter content.
|
||||
|
||||
Privacy visibility mapping:
|
||||
|
||||
|
|
|
|||
|
|
@ -44,11 +44,16 @@ Before deployment:
|
|||
## Deployment Procedure Requirements
|
||||
Required high-level procedure:
|
||||
|
||||
1. Transfer approved artifact to remote host staging area.
|
||||
2. Extract artifact to a clean temporary directory on remote host.
|
||||
3. Validate extracted plugin directory structure.
|
||||
4. Synchronize extracted plugin directory to deploy directory.
|
||||
5. Run post-deploy verification checks.
|
||||
1. Build a fresh deploy artifact as part of deploy execution (package step) using the current repository state.
|
||||
2. Transfer approved artifact to remote host staging area.
|
||||
3. Extract artifact to a clean temporary directory on remote host.
|
||||
4. Validate extracted plugin directory structure.
|
||||
5. Synchronize extracted plugin directory to deploy directory.
|
||||
6. Run post-deploy verification checks.
|
||||
|
||||
Additional policy requirement:
|
||||
- Deploy flow should not bypass packaging by deploying an arbitrary stale artifact path; deployment must use the freshly built artifact for that deploy run.
|
||||
- Deployment tooling may support an explicit version override for controlled releases (for example `1.0.0`); when used, that explicit version must be the packaged and deployed artifact version for that run.
|
||||
|
||||
## Exact-Match Validation (Required)
|
||||
After deployment, deployed plugin files must exactly match the approved artifact contents (excluding allowed mutable runtime files if any are explicitly listed).
|
||||
|
|
|
|||
|
|
@ -61,8 +61,10 @@ Required steps:
|
|||
1. Create/clean a staging folder under `package/`.
|
||||
2. Copy approved runtime files from `code/` into staging.
|
||||
3. Apply exclusion rules to remove non-runtime artifacts.
|
||||
4. Create a versioned zip archive in `package/`.
|
||||
5. Record artifact name and version in release notes/changelog.
|
||||
4. Increment plugin patch version (`X.Y.Z -> X.Y.(Z+1)`) for each package build unless an explicit version override is provided.
|
||||
5. Keep runtime-visible version fields synchronized for the package (plugin header version and health/API version metadata).
|
||||
6. Create a versioned zip archive in `package/`.
|
||||
7. Record artifact name and version in release notes/changelog.
|
||||
|
||||
Artifact naming requirement:
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ Minimum display requirements:
|
|||
- Category (if provided)
|
||||
- Description excerpt/summary (if configured for display)
|
||||
|
||||
List-view formatting requirements:
|
||||
|
||||
- In `list` view, each row headline must present date, time range (or all-day marker), then title in readable natural-language order.
|
||||
- Date formatting in `list` view should use long-form style (for example `5 April 2026`) rather than compact numeric-only format.
|
||||
- Time range formatting in `list` view should be compact and human-readable (for example `9–10am`).
|
||||
- Title must be sourced from event title data and must not be replaced by description text.
|
||||
- If description is shown in `list` view, it should appear as secondary text below the headline.
|
||||
- `list` view rows must not display default browser list bullets.
|
||||
|
||||
Privacy display rules:
|
||||
|
||||
- Public events render full details per normal display rules.
|
||||
|
|
@ -119,6 +128,9 @@ Requirements:
|
|||
- Single-occurrence selection must be user-friendly via a date list or compact calendar selection.
|
||||
- The compact selection mode should support a 3-month grid with previous/next navigation.
|
||||
- The compact selection grid must remain readable in modal layout.
|
||||
- Login, Event Details, and Event Editor overlays must render above site/theme chrome (for example header/banner artwork) and remain interactable without requiring page scroll workarounds.
|
||||
- Login overlay should be centered within the viewport on desktop and mobile.
|
||||
- Event click interactions must use in-page overlays and must not fall back to browser-native dialog boxes.
|
||||
|
||||
## Login and Access Modes
|
||||
- `/calendar` must support two user modes: public (not logged in) and logged-in.
|
||||
|
|
@ -131,6 +143,8 @@ Requirements:
|
|||
- Login dialog must support password-reset request initiation.
|
||||
- Logged-in but non-approved users remain read-only.
|
||||
- Approved users can perform event CRUD.
|
||||
- Event-click behavior must be consistent between normal and private/incognito browser sessions.
|
||||
- In public mode, clicking an event must open the Event Details overlay (not the Event Editor overlay).
|
||||
|
||||
## ICS Link in Web UI
|
||||
The UI must include a user-visible link to an ICS representation of calendar data.
|
||||
|
|
@ -162,3 +176,5 @@ Acceptance should verify:
|
|||
- Empty-state behavior is clear and user-friendly.
|
||||
- ICS link is present and returns valid calendar payload.
|
||||
- Privacy redaction behavior is correct in public views, sidebar, and logged-in views.
|
||||
- In both normal and private/incognito sessions, clicking an event in public mode opens the same Event Details overlay.
|
||||
- Login/Event Details/Event Editor overlays remain above site header/banner layers and are fully usable without scrolling to bypass theme artwork.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ fi
|
|||
|
||||
ARTIFACT=""
|
||||
WP_ROOT="${REMOTE_WP_PATH:-/var/www/wordpress}"
|
||||
VERSION_OVERRIDE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -22,17 +23,24 @@ while [[ $# -gt 0 ]]; do
|
|||
WP_ROOT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION_OVERRIDE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
cat <<'USAGE'
|
||||
Deploy a plugin artifact to remote WordPress.
|
||||
|
||||
Usage:
|
||||
scripts/deploy_remote.sh [--artifact /abs/or/relative/path.zip] [--wp-root /var/www/wordpress]
|
||||
scripts/deploy_remote.sh [--wp-root /var/www/wordpress] [--version X.Y.Z]
|
||||
|
||||
Defaults:
|
||||
- Artifact: latest ./package/calendar-plugin-*.zip
|
||||
- Always builds a fresh package first (which auto-bumps patch version)
|
||||
- Remote host settings from credentials/.env
|
||||
|
||||
Notes:
|
||||
- Use `--version` for explicit release version packaging (for example `1.0.0`).
|
||||
|
||||
This script enforces ownership:
|
||||
- chown -R www-data:www-data <remote plugin dir>
|
||||
USAGE
|
||||
|
|
@ -45,10 +53,20 @@ USAGE
|
|||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${ARTIFACT}" ]]; then
|
||||
ARTIFACT="$(ls -1 "${ROOT_DIR}/package/calendar-plugin-"*.zip 2>/dev/null | sort -V | tail -n1 || true)"
|
||||
if [[ -n "${ARTIFACT}" ]]; then
|
||||
echo "[deploy] --artifact is not supported; deploy always builds a fresh package with bumped patch version" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "${VERSION_OVERRIDE}" ]]; then
|
||||
echo "[deploy] packaging before deployment (explicit version ${VERSION_OVERRIDE})"
|
||||
"${ROOT_DIR}/scripts/package_plugin.sh" --version "${VERSION_OVERRIDE}"
|
||||
else
|
||||
echo "[deploy] packaging before deployment (auto patch bump)"
|
||||
"${ROOT_DIR}/scripts/package_plugin.sh"
|
||||
fi
|
||||
|
||||
ARTIFACT="$(ls -1 "${ROOT_DIR}/package/calendar-plugin-"*.zip 2>/dev/null | sort -V | tail -n1 || true)"
|
||||
if [[ -z "${ARTIFACT}" ]]; then
|
||||
echo "[deploy] no artifact found; run scripts/package_plugin.sh first" >&2
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -36,15 +36,25 @@ USAGE
|
|||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
VERSION="$(sed -n 's/^ \* Version: \(.*\)$/\1/p' "${ROOT_DIR}/code/calendar-plugin.php" | head -n1 | tr -d '[:space:]')"
|
||||
fi
|
||||
CURRENT_VERSION="$(sed -n 's/^ \* Version: \(.*\)$/\1/p' "${ROOT_DIR}/code/calendar-plugin.php" | head -n1 | tr -d '[:space:]')"
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
if [[ -z "${CURRENT_VERSION}" ]]; then
|
||||
echo "[package] unable to detect plugin version from code/calendar-plugin.php" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
if [[ "${CURRENT_VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))"
|
||||
else
|
||||
echo "[package] current version is not semantic (X.Y.Z): ${CURRENT_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -Ei "s/^ \* Version: .*/ * Version: ${VERSION}/" "${ROOT_DIR}/code/calendar-plugin.php"
|
||||
sed -Ei "s/('version' => ')[^']+(',)/\1${VERSION}\2/" "${ROOT_DIR}/code/src/Plugin.php"
|
||||
|
||||
SLUG="calendar-plugin"
|
||||
PACKAGE_DIR="${ROOT_DIR}/package"
|
||||
STAGING_DIR="${PACKAGE_DIR}/staging/${SLUG}"
|
||||
|
|
@ -67,5 +77,6 @@ rsync -a --delete "${ROOT_DIR}/code/" "${STAGING_DIR}/"
|
|||
find . -type f -print0 | sort -z | xargs -0 sha256sum
|
||||
) > "${MANIFEST}"
|
||||
|
||||
echo "[package] version: ${CURRENT_VERSION} -> ${VERSION}"
|
||||
echo "[package] created artifact: ${ARTIFACT}"
|
||||
echo "[package] created manifest: ${MANIFEST}"
|
||||
|
|
|
|||
|
|
@ -401,6 +401,64 @@ if [[ "${CALDAV_COLLECTION_GET}" != "200" ]]; then
|
|||
record_fail "caldav collection GET expected 200 got ${CALDAV_COLLECTION_GET} (${CALDAV_COLLECTION_URL})"
|
||||
fi
|
||||
|
||||
step "caldav DESCRIPTION ALTREP parsing regression"
|
||||
ALTREP_UID="remote-caldav-altrep-$(date +%s)@calendar-plugin"
|
||||
ALTREP_RESOURCE="remote-altrep-$(date +%s).ics"
|
||||
TMP_ALTREP_ICS="$(mktemp)"
|
||||
cat > "${TMP_ALTREP_ICS}" <<ICS
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Remote Regression//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:${ALTREP_UID}
|
||||
SUMMARY:Remote CalDAV ALTREP Regression
|
||||
DTSTART;TZID=Europe/London:20260428T120000
|
||||
DTEND;TZID=Europe/London:20260428T130000
|
||||
DESCRIPTION;ALTREP="data:text/html,test%C2%A0":test
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
ICS
|
||||
ALTREP_PUT_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X PUT \
|
||||
-H 'Content-Type: text/calendar; charset=utf-8' \
|
||||
--data-binary @"${TMP_ALTREP_ICS}" \
|
||||
-o /tmp/remote_test_altrep_put.out -w '%{http_code}' \
|
||||
"${CALDAV_COLLECTION_URL}${ALTREP_RESOURCE}" || true)"
|
||||
rm -f "${TMP_ALTREP_ICS}"
|
||||
if [[ "${ALTREP_PUT_HTTP}" != "201" && "${ALTREP_PUT_HTTP}" != "204" ]]; then
|
||||
record_fail "caldav ALTREP PUT failed (http ${ALTREP_PUT_HTTP})"
|
||||
else
|
||||
ALTREP_EVENT_ID=""
|
||||
ALTREP_LIST_HTTP="$(curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -o /tmp/remote_test_altrep_list.json -w '%{http_code}' \
|
||||
"${BASE_URL}/wp-json/calendar/v1/events" || true)"
|
||||
if [[ "${ALTREP_LIST_HTTP}" != "200" ]]; then
|
||||
record_fail "caldav ALTREP event list fetch failed (http ${ALTREP_LIST_HTTP})"
|
||||
else
|
||||
ALTREP_EVENT_ID="$(python3 - "${ALTREP_UID}" <<'PY'
|
||||
import json,sys
|
||||
uid=sys.argv[1]
|
||||
payload=json.load(open('/tmp/remote_test_altrep_list.json'))
|
||||
for event in payload.get("data", []):
|
||||
if event.get("uid") == uid:
|
||||
print(event.get("id", ""))
|
||||
break
|
||||
else:
|
||||
print("")
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
if [[ -z "${ALTREP_EVENT_ID}" ]]; then
|
||||
record_fail "caldav ALTREP event not found via API list"
|
||||
else
|
||||
if ! curl -fsS -u "${AUTH_USER}:${AUTH_PASS}" "${BASE_URL}/wp-json/calendar/v1/events/${ALTREP_EVENT_ID}" >/tmp/remote_test_altrep_event.json; then
|
||||
record_fail "caldav ALTREP event fetch failed"
|
||||
elif ! json_assert /tmp/remote_test_altrep_event.json "data.get('data', {}).get('description') == 'test'"; then
|
||||
record_fail "caldav ALTREP description parse regression (expected 'test')"
|
||||
fi
|
||||
curl -sS -u "${AUTH_USER}:${AUTH_PASS}" -X DELETE \
|
||||
"${BASE_URL}/wp-json/calendar/v1/events/${ALTREP_EVENT_ID}" >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
step "caldav sync-collection stability"
|
||||
SYNC_UID="remote-caldav-sync-del-$(date +%s)@calendar-plugin"
|
||||
SYNC_CREATE_JSON=$(cat <<JSON
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ Use a minimal subset:
|
|||
- `pending_approval` or unverified user: cannot authenticate.
|
||||
- `active` user: can discover/read/create/update/delete one event successfully.
|
||||
- Monthly ordinal recurrence must round-trip for CalDAV writes (`BYDAY=2SA` and `BYSETPOS=-1` cases).
|
||||
- Thunderbird-style `DESCRIPTION;ALTREP="data:text/html,..."` updates must persist clean plain-text `DESCRIPTION` values without leaking `ALTREP` parameter content into stored descriptions.
|
||||
|
||||
### SMK-007 API Smoke (If API Exposed)
|
||||
- Event create/list/delete basic path.
|
||||
|
|
|
|||
Loading…
Reference in New Issue