Now that we have the URI object, we don't need to convert the Actor to JSONLD
to get its resource ids. Instead we can have shared getters that expose the ids
as a URI, that can be realized as/when they're needed with the base URL.
This makes the code a little cleaner and more performant.
refs #20197
- adds a jackspeak resolution to Ghost core so we can try and ensure the compatible version of jackspeak/string-width is used when the lockfile is regenerated
ref https://linear.app/tryghost/issue/MOM-108
Apologies to my future self and maintainers if you come across this commit.
This is a bit of a mega commit because we need to cut corners somewhere and it
came down to commit atomicity or tests/code quality.
The main changes here are a bunch of tests, as well as some scaffolding for
Inbox handling of Activities and delivery of Activities. The structure is not
final at all - and we have logic split across services which isn't ideal - but
thsi will do for now as we play around and discover the structure through
building.
With TypeScript, when creating mock instances, it's preferable to maintain a
reference to the params, even if they're not used. This allows us to have
unused variables as long as they're prefixed with an underscore.
ref https://linear.app/tryghost/issue/MOM-73
We've made it easier to use by adding defaults for required header, as well as
adding support for signing POST requests.
ref https://linear.app/tryghost/issue/MOM-73
We need to add body parsing middleware here, so that NestJS has access to it.
We also attach the rawBody which is used to validate the HTTP Signatures
closes https://linear.app/tryghost/issue/MOM-80
- updated internal linking search results items
- removed visibility text from meta data
- added additional icon for paid/specific tier visibility
- added titles to icons
- bumped `@tryghost/koenig-lexical` to include support for meta icon titles
- bumped other Koenig packages due to sub-dependency updates
ref DES-228
This PR updates messaging and error handling in order to make Ghost calmer and friendlier. High level summary of the changes:
- Removed all onBlur validation in Settings -> now it’s possible to just click around without being warned to fill mandatory fields
- Removed lot of technical errors like `ValidationError: Validation (isEmpty) failed for locale`
- Completely removed the red background toast notifications, it was aggressive and raw esp. on the top
- Removed some unnecessary notifications (e.g. when removing a webhook, the removal already communicates the result)
- Now we show field errors on submitting forms, and in case of an error we show a “Retry” button in Settings too. This allowed to remove a lot of unnecessary error messages, like the big error message on the top, plus it’s consistent with the patterns outside Settings.
- Notification style is white now with filled color icons which makes everything much calmer and more refined.
- Removes redundant copy (e.g. "successful(ly)") from notifications
---------
Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
ref https://linear.app/tryghost/issue/CFR-27
- updated packages to include performance improvement for NQL filter
strings including multiple neq filters for the same resource
- bumped `bookshelf-plugins`
- bumped NQL versions
We identified a performance fix that allows us to combine not equal
(neq) filters for the same resource in a logically-equivalent way that
also has far more performant resulting SQL.
We're effectively automatically combining strings like
'tag:-tag1+tag:-tag2` into 'tag:-[tag1,tag2]'.
- it appears as though we only accept `err` when it's in the constructor
of the IncorrectUsageError, so in its current form, it is ignored
- this commit performs a minor refactor to switch to constructing a new
IncorrectUsageError and then throwing it
- detected by tsserver complaining about the `err` property not existing
on the error
Data generator uses CSV imports for a massive speed increase, but
can't be used in some environments where SQL admin isn't
available. This allows us to set a flag to use the original
insert-based importer.
fix https://linear.app/tryghost/issue/SLO-104/cannot-read-properties-of-undefined-reading-0-an-unexpected-error
- if the request body didn't contain the correct keys, it'd just HTTP
500 out of there
- this adds some optional chaining so we end up with undefined if
anything isn't as expected, and the following if-statement does the
rest of the check for us
- this also adds a breaking test (the first E2E test for authentication, yay!)
closes https://linear.app/tryghost/issue/MOM-80
- bumps @tryghost/koenig-lexical to add support for search result metadata in internal links as well as some improvements to the internal linking UI/UX
- updates search service to fetch and expose additional `visibility` and `published_at` fields for post/page resources
- updates `searchLinks` method passed to editor to decorate the search results with appropriate meta text and icon based on publish date, post visibility and member settings
closes https://linear.app/tryghost/issue/MOM-106
- the search results can hide any matching authors/tags due to them appearing after matching posts which is typically a longer list that needs scrolling through
- changed the order to list matched authors and tags before posts, this matches the behaviour in our front-end search
refs https://docs.sentry.io/platforms/javascript/configuration/filtering/#using--1%20
- this simplifies our logic to determine whether we should send events
by moving the code to `beforeSend`
- `errorHandler` is going away in Sentry v8 so this results in a shorter
diff in the future
- the logic should be the same, always send non-Ghost errors, and only
send HTTP 500 Ghost errors
- due to the structure of our API controllers, the docName and methods
are under the same structure
- this code loops over the keys of the controller and forms the method
map
- however, it currently also loops over every character of the docName,
so the resulting map contains a weird structure of chars
- we don't need the docName for this, so we can just exclude it from the
keys
- this doesn't change any functionality
fix https://linear.app/tryghost/issue/SLO-101/http-500-with-invalid-multipart-data
- previously, busboy would error out if we supplied a body that was
invalid (such as an empty FormData)
- we would then return a HTTP 500 to the user, which causes all manner
of problems
- now we catch errors from busboy and return a nice BadRequestError
fix https://linear.app/tryghost/issue/SLO-85/fix-http-500-on-contentposts
- in the event we give the incorrect format in a filter, MySQL will
throw an error and we'll throw a HTTP 500 error
- we can capture this error and return a more useful error to the user
- ideally we'd do this in a validation step before attempting the query,
but parsing this out of NQL and detecting which columns are DATETIME
could be quite tricky
- this updates a bunch of places where we're just using Object to cheat
the system
- doing this means editor autocomplete and basic type checking is better
because we now have proper types in place
- functionality should not change, these are just comments
closes https://linear.app/tryghost/issue/MOM-101
- we were mapping over the grouped search results which meant we still got a group even if it's options/items list was empty after filtering for published
closes https://linear.app/tryghost/issue/MOM-103
- the `yield waitForProperty(...)` call that was supposed to return once the content refresh occurred never reached a valid state so the first search query (or any later query) where a content refresh occurred would never resolve causing search to look like it had stalled
- switched to waiting for the last running task to resolve instead which does the same as the previous code intended
- exported the `getPosts` request handler function so in mirage config so we can re-use it with different timing on a per-case basis
fix https://linear.app/tryghost/issue/SLO-87/cannot-read-properties-of-undefined-reading-createimpl-an-unexpected
refs https://github.com/jsdom/jsdom/issues/3709
- in the event we are given some HTML to parse, and that fails, we
currently return a HTTP 500 because it's unhandled
- the instance we saw was due to `<constructor>` crashing jsdom, we've
opened an issue for that
- in terms of handling the error gracefully, we can surround the code
in a try-catch and return a more suitable error. I've gone for a
ValidationError for now - you could debate whether a different one is
more appropriate
- also added Sentry error capturing so we're not blind to these,
ultimately we should make sure the parser can handle all
user-submitted data
closes https://linear.app/tryghost/issue/MOM-97
The 30s search content expiry didn't really make sense and caused unnecessary delays and server load now that search will be more widely used within the editor.
- replaced concept of time-based expiry with explicit expiry
- content still fetched on query if not already loaded or marked as stale
- added `.expireContent()` method on search service to allow explicit expiry
- updated editor to pre-fetch search content when not already loaded or marked as stale
- removes delay when first using internal linking search inside the editor
- updated post model to expire search content on save
- expires on published post save or delete
- expires on publish and unpublish
- updated tag model to expire content on create/save/delete
- only expires when name or url is changed
- updated user model to expire on save/delete
- only expires when name or url is changed
- does not handle creation because that's done server-side via invites
- this adds a simple set of types to the @tryghost/api-framework
package that should describe all of the keys available on a
controller, and then rolls it out to all API controllers
- unfortunately, due to https://github.com/microsoft/TypeScript/issues/47107, we have
to split apart `module.exports` into a variable assignment in order for type-checking
to be done
- the main benefit of this is that `frame` is now typed, and editors understand what keys
are available, so intellisense works properly
- `statusCode` should be a number, but we were passing a string
- this doesn't really affect anything, but tsserver was flagging it up
as the wrong type
- we should pass it as `err` and not `error`
- this probably slipped in because the catch parameter is called
`error`, so I've updated that and fixed the references
- this helps tsserver figure out what the type of things is around our
codebase
- nothing crazy, mostly Express types for the middleware, application and router levels
closes https://linear.app/tryghost/issue/MOM-103
- the `yield waitForProperty(...)` call that was supposed to return once the content refresh occurred never reached a valid state so the first search query (or any later query) where a content refresh occurred would never resolve causing search to look like it had stalled
- switched to waiting for the last running task to resolve instead which does the same as the previous code intended
- exported the `getPosts` request handler function so in mirage config so we can re-use it with different timing on a per-case basis
closes https://linear.app/tryghost/issue/MOM-101
- we were mapping over the grouped search results which meant we still got a group even if it's options/items list was empty after filtering for published
fix https://linear.app/tryghost/issue/SLO-95/unexpected-end-of-multipart-data-for-broken-image-upload-request
- in the event the client sends an invalid body to the image or media
upload endpoints, Dicer will throw an error if the boundary data is
malformed
- previously, we've just been bubbling that up as an InternalServerError
and that results in an HTTP 500
- we can capture errors produced by dicer and return a handled
BadRequestError, as it's the client's fault
- also includes breaking tests
fix https://linear.app/tryghost/issue/SLO-94/unexpected-field-when-given-broken-image-upload-request
- in the event the body of an image or media upload request is malformed
(broken metadata / blob or something), we get a MulterError and this
bubbles up as an InternalServerError and spits out a HTTP 500
- we can capture this and return a BadRequestError, as it's the client's
fault for not providing the correct body
- this implements that and adds breaking tests
fix https://linear.app/tryghost/issue/SLO-96/invalid-version-must-be-a-string-got-type-object-an-unexpected-error
- in the event that a non-semver Accept-Version header is given, the
current code will throw an error because the semver lib can't compare null
against a valid version
- the error in question is `Must be a string. Got type "object"`
- to fix this, we can just detect a null and early return with a
BadRequestError
- also adds a breaking test
ref https://linear.app/tryghost/issue/MOM-72
This module handles signing and validating HTTP signatures, which is a core
part of interfacing with ActivityPub enabled servers.
ref
https://linear.app/tryghost/issue/ENG-902/add-an-optional-timeout-in-the-redis-cache-adapter-in-case-redis
- Added an optional timeout parameter to AdapterCacheRedis, so that the
`get(key)` method will return `null` after the timeout if it hasn't
received a response from Redis
- When load testing the `LinkRedirectRepository` with the Redis cache
enabled on staging, we noticed that for some reason Redis stopped
responding to commands for ~30 seconds.
- The `LinkRedirectRepository` was waiting for the Redis cache to
respond and resulted in a drastic increase in response times for link
redirects
- This change will allow us to set a timeout on the `get(key)` method,
so that if Redis doesn't respond within the timeout, the method will
return `null` as if it were a cache miss.
- Then the `LinkRedirectRepository` will fall back to the database and
return the link redirect from the database instead of waiting
indefinitely for Redis to respond
fix https://linear.app/tryghost/issue/SLO-93/undefined-path-error-with-bad-image-upload
- in the event we receive a request to upload an image, that doesn't
contain an image, we still try and unlink the files
- this is a dangling promise, so it doesn't cause an explicit HTTP
error, but it does show up as a console error
- fixed it by checking for the path, and early returning if it doesn't
exist
- also added a test that would fail without this
ref https://linear.app/tryghost/issue/SLO-78
- the `POST /members/api/member` endpoint is solely used by the alpha
feature `membersSpamPrevention` and should not be available otherwise
fix https://linear.app/tryghost/issue/SLO-88/typeerror-cannot-read-properties-of-null-reading-relations
- in the event that we make it through the version mismatch code, but
without a key, which is possible if you send a request like POST
/ghost/api/v2/content/posts/`, then the version mismatch code will try
and look up the API key attached to a null key, which won't work
- we should handle this case and soft return, to avoid trying to read
`.relations` from `null`
- I'm not entirely convinced by how this code works in general, it seems
quite confusing to reason about, but this commit should solve the HTTP
500 we've been seeing from this
- perhaps in the future we can return earlier in the flow if we receive
a `null` key
ref https://linear.app/tryghost/issue/ENG-881/stripe-tax-checkout-instantiation-fails-for-free-members-when-choosing
- For existing customers to be able to upgrade their account with automatic tax enabled, we need to pass in `customer_update[address]:auto` as per Stripe documentation.
- Automatic tax calculation in Checkout requires a valid address on the Customer. Add a valid address to the Customer or set either 'customer_update[address]' to 'auto' or 'customer_update[shipping]' to 'auto' to save the address entered in Checkout to the Customer.
- We update the existing customer details by passing in address `auto` when they upgrade their accounts.
- Stripe captures the billing address information by default when new accounts are created and then that is used to calculate the tax rate.
fix https://linear.app/tryghost/issue/SLO-82/query-error-unexpected-character-in-filter-at-char-1
- previously, we weren't handling a parsing error, and just bubbling it
back up the chain
- this would result in an InternalServerError somewhere, which caused
500s
- we can handle this, because it's just a bad filter
- this adds handling so we return a 422 upon receiving an invalid filter
fix https://linear.app/tryghost/issue/SLO-79/incorrectusageerror-the-url-httpsblogkongregatecompercentc0-couldnt-be
- we added this Sentry captureException whilst fixing a bug where
decodeUrl could fail, and throw a 500 exception
- we added handling for that case and returned an empty string, but we
also added Sentry error capturing
- at this point, I don't think we need to be capturing errors in Sentry,
because the issue is already handled, and it only usually happens with
malicious/incorrect URLs
- this is our #2 cause of Sentry alerts, so it's good to clean it up
ref https://linear.app/tryghost/issue/ENG-881/stripe-tax-checkout-instantiation-fails-for-free-members-when-choosing
- For existing customers to be able to upgrade their account with automatic tax enabled, we need to pass in `customer_update[address]:auto` as per Stripe documentation.
- Automatic tax calculation in Checkout requires a valid address on the Customer. Add a valid address to the Customer or set either 'customer_update[address]' to 'auto' or 'customer_update[shipping]' to 'auto' to save the address entered in Checkout to the Customer.
- We update the existing customer details by passing in address `auto` when they upgrade their accounts.
- Stripe captures the billing address information by default when new accounts are created and then that is used to calculate the tax rate.
- we don't need to deep require into the library as it exports what we
need on the surface
- this should unblock https://github.com/TryGhost/Ghost/pull/19002, as
it's randomly failing with this require
refs
[ENG-827](https://linear.app/tryghost/issue/ENG-827/🐛-crash-on-resizing-animated-gif)
Added a timeout to the image resizing middleware to prevent crashes when
an image is taking too long to resize. When the timeout is reached and
the image has not been resized, the middleware will return the original
image
closes https://linear.app/tryghost/issue/MOM-60
- when the dropdown opens near the end of the document, clicking the links sometimes did nothing and showed an error in the console
- we have a mousedown event handler on an element that surrounds the main editing element that re-focuses the editor when clicked in order to make the "focus editor" click target larger and more natural-feeling but it was inadvertently re-focusing when the mousedown event fired for an element in the dropdown list when the list was positioned outside of the main editor element. This lead to timing issues with the bookmark node being removed on blur because it was empty followed by an error from the node's component's async event handlers which were trying to set values on the now-removed node
- by switching from `event.target.closest()` to looping over `event.composedPath()` when checking to see if we should skip re-focusing we're more resilient to DOM manipulations occurring between event triggers and function calls because we'll always be given the list of elements that existed at the time the event fired
reduce word size to fit properly within button without making style
changes (_economize_ and _poupe_ have the exact same meaning)
Co-authored-by: Ryan Feigenbaum <48868107+royalfig@users.noreply.github.com>
ref
https://linear.app/tryghost/issue/ENG-851/implement-a-minimal-but-complete-version-of-redirect-caching-to
ref https://app.incident.io/ghost/incidents/55
Often immediately after sending an email, sites receive a large volume
of requests to LinkRedirect endpoints from members clicking on the links in
the email.
We currently don't cache any of these requests in our CDN, because we
also record click events, update the member's `last_seen_at` timestamp,
and send webhooks in response to these clicks, so Ghost needs to handle
each of these requests itself. This means that each of these LinkRedirect requests
hits Ghost, and currently all these requests hit the database to lookup
where to redirect the member to.
Each one of these requests can make up to 11 database queries, which can
quickly exhaust Ghost's database connection pool. Even though the
LinkRedirect lookup query is fairly cheap and quick, these queries aren't
prioritized over the "record" queries Ghost needs to handle, so they can
get stuck behind other queries in the queue and eventually timeout.
The result is that members are unable to actually reach the destination
of the link they clicked on, instead receiving a 500 error in Ghost, or
it can take a long time (60s+) for the redirect to happen.
This PR uses our existing `adapterManager` to cache the redirect lookups
either in-memory or in Redis (if configured — by default there is no caching). This only removes 1 out of
11 queries per redirect request, so it won't reduce the load on the DB
drastically, but it at least decouples the serving of the LinkRedirect from
the DB so the member can be redirected even if the DB is under heavy
load.
Local load testing results have shown a decrease in response times from
60 seconds to ~50ms for the redirect requests when handling 500 requests
per second, and reduced the 500 error rate to 0.
ref https://linear.app/tryghost/issue/ENG-826
- Changed staff deletion logic to do a bulk insert when adding a tag to
the users' associated posts
Staff deletion logic has really poor performance at scale because we do
individual updates for every post. If a user has dozens+ posts
(especially in a large db with thousands of posts), this can take >60s
and look like a timeout. Ultimately this should probably be a jobbed off
process, but for the time being we can improve this by doing a bulk
insert.
Note that this update uses the pattern for the bulk tagging of posts
from the right click (bulk) actions in the posts lists in Admin. With
bulk actions, **we do not trigger web hooks or the post.edited events**.
We will document this and follow up on this separately.
ref MOM61
- Adds admin-x react app we’ll use as ActivityPub playground to the
sidebar nav behind the feature flag.
- Wired up routing to Ember
- Setup the project as `admin-x-activitypub`
---------
Co-authored-by: Ronald Langeveld <hi@ronaldlangeveld.com>
ref https://linear.app/tryghost/issue/MOM-29
This is very rough, and all still behind a flag. The idea is that any public
post which is published gets added to the Outbox of the site Actor. We also
dispatch an event, which will be used to deliver the Activity to any relevant
inboxes, but that is outside the scope of this commit.
ref https://linear.app/tryghost/issue/MOM-25
All these intersection types are getting a bit out of hand - but we can clean
up all of this once we're past prototyping phase.
refs https://ghost-foundation.sentry.io/issues/5135326925/
- the service tends to 503 all the time, and we don't really care enough
for it to ping us in Sentry, as it's not something we control
- we can still keep logging the errors in case we need to go and look at
what went wrong
no issue
- The `RedirectsImporter` used by the data generator was creating
redirects with the wrong length for the `from` field, which didn't match
the actual behavior of Ghost.
- This commit corrects the length from 32 to 8, which is the actual
length of the `from` field in production.
- This change has no impact on Ghost's behavior, but makes the data
generator more representative of real world data for more accurate
testing.
fixes
https://linear.app/tryghost/issue/DES-260/footer-link-text-smaller-than-regular-text
There was a bit of CSS in a media query aimed at other parts of the
newsletter template that was causing the footer styling to break. I
added some more specific styling for the footer as well, to make sure
span's within the `<p>` element are covered as well.
closes https://linear.app/tryghost/issue/MOM-49
- bumped koenig-lexical so the bookmark card has group support for testing
- updated `searchLinks` function passed to Koenig to match expected grouped results shape
ref https://linear.app/tryghost/issue/MOM-25
This matches the way that mastodon handles the key url and may be the reason
these documents are incompatible. This also removes the `username` key as that
isn't used anywhere, instead we have a username property which is rendered as
the ActivityPub compat preferredUsername key.