Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion helpers/undici/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const copies = {
};

export const bundledVersion = process.versions.undici;
export const packageName = copies[Number(bundledVersion.split(".", 1)[0])];
export const packageName = copies[Number(bundledVersion?.split(".", 1)[0])];

if (!packageName) {
throw new Error(
Expand Down
1 change: 1 addition & 0 deletions jsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"target": "ESNext",
"types": [],
"useUnknownInCatchVariables": true
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-auth/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const getApplicationInformation = (body, client) => {
for (const item of items) {
const { properties, type } = item;

if (/^h-(?:x-)?app$/.test(type[0])) {
if (type && /^h-(?:x-)?app$/.test(type[0])) {
// If no URL property, use baseUrl
if (!properties.url) {
properties.url = [client.url];
Expand Down
6 changes: 5 additions & 1 deletion packages/endpoint-auth/lib/controllers/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export const introspectionController = {

if (!token) {
// Remove ‘Bearer ’ from authorization header
token = request.headers.authorization.trim().split(/\s+/, 2)[1];
token = request.headers.authorization?.trim().split(/\s+/, 2)[1];
}

if (!token) {
return response.json({ active: false });
}

let accessToken = verifyToken(token);
Expand Down
4 changes: 4 additions & 0 deletions packages/endpoint-auth/lib/controllers/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const userinfoController =
try {
// Remove ‘Bearer ’ from authorization header
const token = request.headers.authorization?.trim().split(/\s+/, 2)[1];
if (!token) {
throw new Error("No token");
}

accessToken = verifyToken(token);
} catch {
throw IndiekitError.unauthorized(
Expand Down
3 changes: 2 additions & 1 deletion packages/endpoint-files/lib/controllers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { getFileName } from "../utils.js";
export const filesController = async (request, response, next) => {
try {
const { application } = request.app.locals;
const { access_token, scope } = request.session;
const access_token = request.session?.access_token;
const scope = request.session?.scope;
const { after, before, success } = request.query;
const limit = Number(request.query.limit) || 20;

Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-files/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getFileProperties = async (uid, mediaEndpoint, accessToken) => {
*/
export const getFileName = (url) => {
const { pathname } = new URL(url);
return pathname.split("/").pop();
return pathname.slice(pathname.lastIndexOf("/") + 1);
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-media/lib/controllers/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const actionController = (imageProcessing) =>

try {
// Check provided scope
const { scope } = session;
const scope = session?.scope;
const hasScope = checkScope(scope);
if (!hasScope) {
throw IndiekitError.insufficientScope(
Expand Down
20 changes: 18 additions & 2 deletions packages/endpoint-media/lib/file.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import path from "node:path";

import { IndiekitError } from "@indiekit/error";
import { getDate, slugify } from "@indiekit/util";
import { fileTypeFromBuffer } from "file-type";

/**
* Get file type from file data
* @param {object} file - File object
* @returns {Promise<import("file-type").FileTypeResult>} File type
*/
const getFileType = async (file) => {
const fileType = await fileTypeFromBuffer(file.data);
if (!fileType) {
// Message is the media type; controller localises the error
throw IndiekitError.unsupportedMediaType(file.mimetype || "unknown");
}

return fileType;
};

/**
* Derive properties from file data
* @param {object} publication - Publication configuration
Expand All @@ -17,7 +33,7 @@ import { fileTypeFromBuffer } from "file-type";
* }
*/
export const getFileProperties = async (publication, file, timeZone) => {
const { ext } = await fileTypeFromBuffer(file.data);
const { ext } = await getFileType(file);
const published = getPublishedProperty(timeZone);

let basename = path.basename(file.name, path.extname(file.name));
Expand All @@ -39,7 +55,7 @@ export const getFileProperties = async (publication, file, timeZone) => {
* @example getMediaType("brighton-pier.jpg") => "photo"
*/
export const getMediaType = async (file) => {
const { mime } = await fileTypeFromBuffer(file.data);
const { mime } = await getFileType(file);
const type = mime.split("/", 1)[0];

if (type === "image") {
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-media/lib/scope.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Check provided scope(s) satisfies required scope
* @param {string} scope - Provided scope (space separated)
* @param {string} [scope] - Provided scope (space separated)
* @param {string} [action] - Required action
* @returns {boolean} `true` if provided scope includes action
*/
Expand Down
18 changes: 18 additions & 0 deletions packages/endpoint-media/test/unit/file.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { strict as assert } from "node:assert";
import { Buffer } from "node:buffer";
import { describe, it } from "node:test";

import { getFixture } from "@indiekit-test/fixtures";
Expand Down Expand Up @@ -35,4 +36,21 @@ describe("endpoint-media/lib/file", () => {
assert.equal(result.md5, "be7d321488de26f2eb38834af7162164");
assert.equal(isValid(parseISO(result.published)), true);
});

it("Throws error if file type can’t be determined", async () => {
const file = {
data: Buffer.from("Plain text"),
mimetype: "text/plain",
name: "notes.txt",
};

await assert.rejects(getMediaType(file), {
name: "UnsupportedMediaTypeError",
message: "text/plain",
status: 415,
});
await assert.rejects(getFileProperties({}, file, "UTC"), {
name: "UnsupportedMediaTypeError",
});
});
});
3 changes: 2 additions & 1 deletion packages/endpoint-micropub/lib/controllers/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const actionController = async (request, response, next) => {

try {
// Check provided scope
const { scope, token } = session;
const scope = session?.scope;
const token = session?.token;
const hasScope = checkScope(scope, action);
if (!hasScope) {
throw IndiekitError.insufficientScope(
Expand Down
3 changes: 3 additions & 0 deletions packages/endpoint-micropub/lib/controllers/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export const queryController = async (request, response, next) => {
return response.json(getMf2Properties(mf2, properties));
}
// Return mf2 for published posts
/**
* @type {{ items: object[], hasNext: boolean, hasPrev: boolean, firstItem?: string, lastItem?: string }}
*/
let cursor = {
items: [],
hasNext: false,
Expand Down
25 changes: 16 additions & 9 deletions packages/endpoint-micropub/lib/jf2.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,19 @@ export const getLocationProperty = (properties) => {
if (typeof location === "string" && location.startsWith("geo:")) {
const geoUriRegexp =
/geo:(?<latitude>[\d+.?-]*),(?<longitude>[\d+.?-]*)(?:,(?<altitude>[\d+.?-]*))?/;
const { latitude, longitude, altitude } =
location.match(geoUriRegexp).groups;

location = {
type: "geo",
latitude,
longitude,
...(altitude && { altitude }),
};
const groups = location.match(geoUriRegexp)?.groups;

// Leave an un-parseable Geo URI as given
if (groups) {
const { latitude, longitude, altitude } = groups;

location = {
type: "geo",
latitude,
longitude,
...(altitude && { altitude }),
};
}
}

return location;
Expand Down Expand Up @@ -278,6 +282,9 @@ export const getSlugProperty = (properties, separator) => {
const suggested = properties["mp-slug"];
const { name, published } = properties;

/**
* @type {string}
*/
let string;
if (suggested) {
string = suggested;
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-micropub/lib/mf2.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Return mf2 properties of a post
* @param {object} mf2 - mf2 object
* @param {Array<string>|string} requestedProperties - mf2 properties to select
* @param {Array<string>|string} [requestedProperties] - mf2 properties to select
* @returns {Promise<object>} mf2 with requested properties
*/
export const getMf2Properties = (mf2, requestedProperties) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/endpoint-micropub/lib/post-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const debug = makeDebug("indiekit:endpoint-micropub:post-data");
/**
* Get post type configuration, which must include a post path
* @param {object} postTypes - Publication post types
* @param {string} type - Post type
* @param {string|null} type - Post type
* @returns {object} Post type configuration
*/
const getTypeConfig = (postTypes, type) => {
const typeConfig = postTypes[type];
const typeConfig = type ? postTypes[type] : undefined;
if (!typeConfig?.post?.path) {
throw IndiekitError.notImplemented(type);
throw IndiekitError.notImplemented(String(type));
}

return typeConfig;
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-micropub/lib/scope.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Check provided scope(s) satisfies required scope
* @param {string} scope - Provided scope (space separated)
* @param {string} [scope] - Provided scope (space separated)
* @param {string} [action] - Required action
* @returns {boolean|string} `true` if provided scope includes action,
* `draft` if draft scope, otherwise `false`
Expand Down
1 change: 1 addition & 0 deletions packages/endpoint-micropub/test/unit/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe("endpoint-micropub/lib/media", () => {
});

it("Throws error no media endpoint URL", async () => {
// @ts-ignore: Testing invalid input
await assert.rejects(uploadMedia(undefined, token, properties, files), {
message: "Failed to parse URL from undefined",
});
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint-posts/lib/controllers/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const newController = {
const { publication } = request.app.locals;
const postsPath = path.dirname(request.baseUrl + request.path);
const postType = request.query.type;
const { scope } = request.session;
const scope = request.session?.scope;

const postTypeItems = Object.values(publication.postTypes)
.toSorted((a, b) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/endpoint-posts/lib/controllers/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { getPostStatusBadges, getPostName, getPhotoUrl } from "../utils.js";
export const postsController = async (request, response, next) => {
try {
const { application, publication } = request.app.locals;
const { access_token, scope } = request.session;
const access_token = request.session?.access_token;
const scope = request.session?.scope;
const { after, before, success } = request.query;
const limit = Number(request.query.limit) || 12;

Expand Down
7 changes: 6 additions & 1 deletion packages/endpoint-posts/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export const getChannelItems = (publication) => {
* @returns {object} JF2 geo location property
*/
export const getGeoProperty = (geo) => {
const { latitude, longitude } = geo.match(ISO_6709_RE).groups;
const groups = geo.match(ISO_6709_RE)?.groups;
if (!groups) {
throw IndiekitError.badRequest(`Invalid geographic coordinates: ${geo}`);
}

const { latitude, longitude } = groups;

return {
type: "geo",
Expand Down
1 change: 1 addition & 0 deletions packages/error/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe("error", () => {
indiekitError();
},
(error) => {
assert.ok(error instanceof IndiekitError);
assert.equal(error.toString(), "IndiekitError: Message");
return true;
},
Expand Down
38 changes: 26 additions & 12 deletions packages/frontend/components/add-another/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getElement } from "../../scripts/utils/get-element";

const focusableSelector = `button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]`;

export const AddAnotherComponent = class extends HTMLElement {
Expand All @@ -22,10 +24,10 @@ export const AddAnotherComponent = class extends HTMLElement {
$list;

connectedCallback() {
this.$addButtonTemplate = this.querySelector("#add-button");
this.$deleteButtonTemplate = this.querySelector("#delete-button");
this.$addButtonTemplate = getElement(this, "#add-button");
this.$deleteButtonTemplate = getElement(this, "#delete-button");
this.$$fields = this.querySelectorAll(".field");
this.$list = this.querySelector(".add-another__list");
this.$list = getElement(this, ".add-another__list");

this.updateItems();
this.createAddButton();
Expand Down Expand Up @@ -70,7 +72,7 @@ export const AddAnotherComponent = class extends HTMLElement {
* @returns {HTMLLegendElement} - Group legend
*/
getHeading() {
return this.querySelector("legend");
return getElement(this, "legend");
}

/**
Expand All @@ -91,14 +93,14 @@ export const AddAnotherComponent = class extends HTMLElement {

this.append($addButton);

$addButton = this.querySelector(".add-another__add");
$addButton = getElement(this, ".add-another__add");
$addButton.addEventListener("click", (event) => this.add(event));
}

/**
* Get delete button
* @param {HTMLElement} element - Containing element
* @returns {HTMLButtonElement} - Delete button
* @returns {HTMLButtonElement|null} - Delete button
*/
getDeleteButton(element) {
return element.querySelector(".add-another__delete");
Expand All @@ -109,8 +111,10 @@ export const AddAnotherComponent = class extends HTMLElement {
* @param {HTMLElement} element - Containing element
*/
createDeleteButton(element) {
const $deleteButton =
this.$deleteButtonTemplate.content.firstElementChild.cloneNode(true);
const $deleteButton = getElement(
this.$deleteButtonTemplate.content,
".add-another__delete",
).cloneNode(true);

element.append($deleteButton);
}
Expand All @@ -121,6 +125,11 @@ export const AddAnotherComponent = class extends HTMLElement {
*/
updateDeleteButton(element) {
const $deleteButton = this.getDeleteButton(element);

if (!$deleteButton) {
return;
}

$deleteButton.setAttribute("aria-labelledby", `delete-title ${element.id}`);
$deleteButton.addEventListener("click", (event) => this.delete(event));
}
Expand Down Expand Up @@ -161,6 +170,11 @@ export const AddAnotherComponent = class extends HTMLElement {
const $$labels = $item.querySelectorAll("label");
for (const $label of $$labels) {
const forAttribute = $label.getAttribute("for");

if (!forAttribute) {
continue;
}

$label.setAttribute("for", forAttribute.replace("-0", `-${uid}`));
}

Expand Down Expand Up @@ -190,13 +204,13 @@ export const AddAnotherComponent = class extends HTMLElement {
this.createDeleteButton($item);
}

const $deleteButton = this.getDeleteButton($item);

// If has delete button
if (this.getDeleteButton($item)) {
if ($deleteButton) {
if ($$items.length === 1) {
// If only 1 item in list, remove button
this.getDeleteButton($item).remove();
$deleteButton.remove();
} else {
// Else update button attributes
this.updateDeleteButton($item);
}
}
Expand Down
Loading
Loading