Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5b4bc2e03 | ||
|
|
416c74ac5b | ||
|
|
fc412e6f2f | ||
|
|
db3658c422 | ||
|
|
6de5299323 | ||
|
|
43816fdecc | ||
|
|
6232333918 | ||
|
|
a1f6e54f9d | ||
|
|
48119f2034 |
+4
-1
@@ -4,7 +4,7 @@ Tags: woocommerce, discount, email, shipping, coupon
|
|||||||
Requires at least: 5.8
|
Requires at least: 5.8
|
||||||
Tested up to: 7.1
|
Tested up to: 7.1
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.4
|
||||||
Stable tag: 1.4.3
|
Stable tag: 1.4.4
|
||||||
WC requires at least: 6.0
|
WC requires at least: 6.0
|
||||||
WC tested up to: 11.1.0
|
WC tested up to: 11.1.0
|
||||||
License: GPLv2 or later
|
License: GPLv2 or later
|
||||||
@@ -96,6 +96,9 @@ The most specific match wins: exact email, then domain, then partial match.
|
|||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.4.4 =
|
||||||
|
* A new version now shows up within about fifteen minutes instead of being hidden behind a six-hour cache. The plugin was holding on to its own answer for longer than WordPress waits before asking again, so a release could be published and the shop shown nothing.
|
||||||
|
|
||||||
= 1.4.3 =
|
= 1.4.3 =
|
||||||
* The plugin is now credited to Kristian Wind, with a link to where its source lives. It previously carried a placeholder name.
|
* The plugin is now credited to Kristian Wind, with a link to where its source lives. It previously carried a placeholder name.
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -18,7 +18,14 @@ while IFS= read -r line; do
|
|||||||
[ -n "$line" ] && patterns+=("${line%/}")
|
[ -n "$line" ] && patterns+=("${line%/}")
|
||||||
done < .distignore
|
done < .distignore
|
||||||
|
|
||||||
git ls-files | while IFS= read -r file; do
|
# -z, not plain `git ls-files`: git QUOTES any path containing a non-ASCII
|
||||||
|
# byte, so "TESTMILJØ.md" comes out as the literal 14-character string
|
||||||
|
# "TESTMILJ\303\230.md". That form matches no pattern in .distignore, so the
|
||||||
|
# file is kept — an internal document walking straight into the release zip
|
||||||
|
# with nothing said. (-z also covers paths with spaces.) Found in a sibling
|
||||||
|
# plugin that has exactly such a filename; this repo has none today, which is
|
||||||
|
# precisely why it would have sat here until someone added one.
|
||||||
|
git ls-files -z | while IFS= read -r -d '' file; do
|
||||||
keep=1
|
keep=1
|
||||||
for p in "${patterns[@]}"; do
|
for p in "${patterns[@]}"; do
|
||||||
# A bare name excludes the file or directory wherever it appears, which is
|
# A bare name excludes the file or directory wherever it appears, which is
|
||||||
|
|||||||
@@ -46,33 +46,51 @@ ci_plugin_header() {
|
|||||||
"$(ci_repo_root)/$PLUGIN_MAIN" | head -1 | sed -E 's/[[:space:]]+$//'
|
"$(ci_repo_root)/$PLUGIN_MAIN" | head -1 | sed -E 's/[[:space:]]+$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Run a shell script against a checkout of this repository, inside a container.
|
||||||
|
#
|
||||||
|
# The repository goes in over a tarball and `docker cp`, never a bind mount: a
|
||||||
|
# Gitea Actions job runs inside a container talking to the HOST's docker
|
||||||
|
# daemon, so a host path here lands on an empty directory nobody created and
|
||||||
|
# the check passes by checking nothing. The symptom reads like a typo in a path.
|
||||||
|
#
|
||||||
|
# Two details are load-bearing. The script is copied in as a FILE rather than
|
||||||
|
# interpolated into `sh -c`, so it can contain $, quotes and newlines without
|
||||||
|
# being mangled — which matters the moment it is more than one command. And
|
||||||
|
# --entrypoint sh is not decoration: some images (composer:2, for one) have an
|
||||||
|
# entrypoint that swallows its arguments.
|
||||||
|
#
|
||||||
|
# Taken from the subscriptions port, which needed it for composer.
|
||||||
|
ci_in_container() { # image script-text
|
||||||
|
local image="$1" script="$2"
|
||||||
|
local dir cid code=0
|
||||||
|
|
||||||
|
dir=$(mktemp -d -t wpci-XXXXXX)
|
||||||
|
( cd "$(ci_repo_root)" && git ls-files -z | tar --null -T - -cf "$dir/src.tar" )
|
||||||
|
printf '%s\n' "$script" > "$dir/run.sh"
|
||||||
|
|
||||||
|
cid=$(docker create --entrypoint sh -w /src "$image" \
|
||||||
|
-c 'mkdir -p /src && tar -xf /tmp/src.tar -C /src && exec sh /tmp/run.sh')
|
||||||
|
docker cp "$dir/src.tar" "$cid:/tmp/src.tar" >/dev/null
|
||||||
|
docker cp "$dir/run.sh" "$cid:/tmp/run.sh" >/dev/null
|
||||||
|
docker start -a "$cid" || code=$?
|
||||||
|
docker rm -f "$cid" >/dev/null
|
||||||
|
rm -rf "$dir"
|
||||||
|
return "$code"
|
||||||
|
}
|
||||||
|
|
||||||
# Run one of this repository's PHP scripts.
|
# Run one of this repository's PHP scripts.
|
||||||
#
|
#
|
||||||
# Uses the local php when there is one — a laptop, GitHub's hosted runners —
|
# Uses the local php when there is one — a laptop, GitHub's hosted runners —
|
||||||
# and a container otherwise. The Gitea runner image has no php, and losing a
|
# and a container otherwise. The Gitea runner image has no php, and losing a
|
||||||
# build to "php: command not found" after everything else passed is a silly way
|
# build to "php: command not found" after everything else passed is a silly way
|
||||||
# to spend an afternoon. Measured: it happened on the very first Gitea run.
|
# to spend an afternoon. Measured: it happened on the very first Gitea run.
|
||||||
#
|
|
||||||
# The repository goes in over a tarball and `docker cp`, never a bind mount: a
|
|
||||||
# Gitea Actions job runs inside a container talking to the HOST's docker
|
|
||||||
# daemon, so a host path here lands on an empty directory nobody created.
|
|
||||||
ci_php() {
|
ci_php() {
|
||||||
if command -v php >/dev/null 2>&1; then
|
if command -v php >/dev/null 2>&1; then
|
||||||
php "$@"
|
php "$@"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local tarball cid code=0
|
ci_in_container "php:8.4-cli" "exec php $(printf '%q ' "$@")"
|
||||||
tarball=$(mktemp -t wpci-php-XXXXXX.tar)
|
|
||||||
( cd "$(ci_repo_root)" && git ls-files -z | tar --null -T - -cf "$tarball" )
|
|
||||||
|
|
||||||
cid=$(docker create "php:8.4-cli" sh -c \
|
|
||||||
"mkdir -p /src && tar -xf /tmp/src.tar -C /src && cd /src && exec php $(printf '%q ' "$@")")
|
|
||||||
docker cp "$tarball" "$cid:/tmp/src.tar" >/dev/null
|
|
||||||
docker start -a "$cid" || code=$?
|
|
||||||
docker rm -f "$cid" >/dev/null
|
|
||||||
rm -f "$tarball"
|
|
||||||
return "$code"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ci_step() { printf '\n\033[1m→ %s\033[0m\n' "$*"; }
|
ci_step() { printf '\n\033[1m→ %s\033[0m\n' "$*"; }
|
||||||
|
|||||||
+58
-7
@@ -65,6 +65,35 @@ tested_wc=$(ci_plugin_header 'WC tested up to')
|
|||||||
body=$(printf '%s\n\n---\n\nTested up to: %s\nWC tested up to: %s\n' \
|
body=$(printf '%s\n\n---\n\nTested up to: %s\nWC tested up to: %s\n' \
|
||||||
"$notes" "${tested_wp:-unknown}" "${tested_wc:-unknown}")
|
"$notes" "${tested_wp:-unknown}" "${tested_wc:-unknown}")
|
||||||
|
|
||||||
|
# Read one top-level field out of a JSON response.
|
||||||
|
#
|
||||||
|
# Not sed. A greedy `.*"id":([0-9]+)` matches the LAST id in the payload, and a
|
||||||
|
# Gitea release carries both an embedded "author" object and, once anything is
|
||||||
|
# attached, an "assets" array — each with an id of its own. Measured on this
|
||||||
|
# repo: the four published releases have ids 416/417/419/421, and the greedy
|
||||||
|
# pattern returns 1/2/3/4, which are the asset ids.
|
||||||
|
#
|
||||||
|
# Those four releases were nonetheless published correctly, and only by luck:
|
||||||
|
# the author id on this instance is **-1**, and the minus sign stops `[0-9]+`
|
||||||
|
# from matching, so on a create response — which has no assets yet — the last
|
||||||
|
# numeric id happened to be the release's. A real author id, or a re-run
|
||||||
|
# against a release that already has its zip, and it picks the wrong number.
|
||||||
|
#
|
||||||
|
# Found by the session porting this into subscriptions, where it picked the
|
||||||
|
# author id and the upload 404'd. python3 is already a required tool here.
|
||||||
|
json_field() { # field (JSON on stdin)
|
||||||
|
python3 -c '
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
data = json.load( sys.stdin )
|
||||||
|
except Exception:
|
||||||
|
sys.exit( 0 )
|
||||||
|
value = data.get( sys.argv[1] )
|
||||||
|
if value is not None:
|
||||||
|
print( value )
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
api() { # method path [data]
|
api() { # method path [data]
|
||||||
curl -fsS -X "$1" \
|
curl -fsS -X "$1" \
|
||||||
-H "Authorization: token $GH_TOKEN" \
|
-H "Authorization: token $GH_TOKEN" \
|
||||||
@@ -94,20 +123,42 @@ print(json.dumps({
|
|||||||
# A re-run of a release must not fail on "already exists".
|
# A re-run of a release must not fail on "already exists".
|
||||||
existing=$(curl -fsS -H "Authorization: token $GH_TOKEN" \
|
existing=$(curl -fsS -H "Authorization: token $GH_TOKEN" \
|
||||||
"$GITHUB_API_URL/repos/$owner/$repo/releases/tags/$TAG" 2>/dev/null || true)
|
"$GITHUB_API_URL/repos/$owner/$repo/releases/tags/$TAG" 2>/dev/null || true)
|
||||||
id=$(printf '%s' "$existing" | sed -n -E 's/.*"id":[[:space:]]*([0-9]+).*/\1/p' | head -1)
|
id=$(printf '%s' "$existing" | json_field id)
|
||||||
|
|
||||||
if [ -z "$id" ]; then
|
if [ -z "$id" ]; then
|
||||||
created=$(api POST "$GITHUB_API_URL/repos/$owner/$repo/releases" "$payload")
|
created=$(api POST "$GITHUB_API_URL/repos/$owner/$repo/releases" "$payload")
|
||||||
id=$(printf '%s' "$created" | sed -n -E 's/.*"id":[[:space:]]*([0-9]+).*/\1/p' | head -1)
|
id=$(printf '%s' "$created" | json_field id)
|
||||||
fi
|
fi
|
||||||
[ -n "$id" ] || { echo "Could not create or find the release for $TAG." >&2; exit 1; }
|
[ -n "$id" ] || { echo "Could not create or find the release for $TAG." >&2; exit 1; }
|
||||||
echo " release id $id"
|
echo " release id $id"
|
||||||
|
|
||||||
curl -fsS -X POST \
|
# Before uploading anything to it, confirm the id really is this tag's. The
|
||||||
-H "Authorization: token $GH_TOKEN" \
|
# failure being guarded against is not a 404 — that is loud — but an id that
|
||||||
-F "attachment=@$zip" \
|
# exists and belongs to something else, which would attach this plugin's zip
|
||||||
"$GITHUB_API_URL/repos/$owner/$repo/releases/$id/assets?name=$PLUGIN_SLUG.zip" >/dev/null
|
# to an unrelated release with every check still green.
|
||||||
echo " attached $PLUGIN_SLUG.zip"
|
release=$(curl -fsS -H "Authorization: token $GH_TOKEN" \
|
||||||
|
"$GITHUB_API_URL/repos/$owner/$repo/releases/$id")
|
||||||
|
actual=$(printf '%s' "$release" | json_field tag_name)
|
||||||
|
if [ "$actual" != "$TAG" ]; then
|
||||||
|
echo "Release id $id is tagged '$actual', not '$TAG'. Refusing to upload the zip to it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A re-run must not fail on the half that already succeeded: the upload
|
||||||
|
# rejects a duplicate asset name.
|
||||||
|
if printf '%s' "$release" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
data = json.load( sys.stdin )
|
||||||
|
sys.exit( 0 if any( a.get( "name" ) == sys.argv[1] for a in data.get( "assets", [] ) ) else 1 )
|
||||||
|
' "$PLUGIN_SLUG.zip"; then
|
||||||
|
echo " $PLUGIN_SLUG.zip is already attached — leaving it alone"
|
||||||
|
else
|
||||||
|
curl -fsS -X POST \
|
||||||
|
-H "Authorization: token $GH_TOKEN" \
|
||||||
|
-F "attachment=@$zip" \
|
||||||
|
"$GITHUB_API_URL/repos/$owner/$repo/releases/$id/assets?name=$PLUGIN_SLUG.zip" >/dev/null
|
||||||
|
echo " attached $PLUGIN_SLUG.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
release_api="$GITHUB_API_URL/repos/$owner/$repo/releases/latest"
|
release_api="$GITHUB_API_URL/repos/$owner/$repo/releases/latest"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+4
-2
@@ -76,11 +76,13 @@ before=$fails
|
|||||||
if git ls-files -z '*.php' | xargs -0 grep -nP '<\?(?!php|=|xml)' ; then
|
if git ls-files -z '*.php' | xargs -0 grep -nP '<\?(?!php|=|xml)' ; then
|
||||||
note_fail "Use <?php, not short open tags."
|
note_fail "Use <?php, not short open tags."
|
||||||
fi
|
fi
|
||||||
while read -r f; do
|
# -z: git quotes non-ASCII paths, and the quoted form is not a path that
|
||||||
|
# exists, so this loop would silently skip the file it was meant to check.
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
[ -n "$f" ] || continue
|
[ -n "$f" ] || continue
|
||||||
[ "$(tail -c 3 "$f" | tr -d '[:space:]')" = "?>" ] \
|
[ "$(tail -c 3 "$f" | tr -d '[:space:]')" = "?>" ] \
|
||||||
&& { ci_annotate error "$f" 1 "Remove the trailing ?> at the end of the file."; fails=$((fails + 1)); }
|
&& { ci_annotate error "$f" 1 "Remove the trailing ?> at the end of the file."; fails=$((fails + 1)); }
|
||||||
done <<< "$(git ls-files '*.php')"
|
done < <(git ls-files -z '*.php')
|
||||||
[ "$fails" = "$before" ] && echo " clean"
|
[ "$fails" = "$before" ] && echo " clean"
|
||||||
|
|
||||||
ci_step "text domain"
|
ci_step "text domain"
|
||||||
|
|||||||
+31
-5
@@ -92,13 +92,39 @@ echo " status: $status"
|
|||||||
[ "$now" = "$expected" ] || { echo "Expected $expected after the update." >&2; exit 1; }
|
[ "$now" = "$expected" ] || { echo "Expected $expected after the update." >&2; exit 1; }
|
||||||
[ "$status" = active ] || { echo "The plugin is no longer active after updating itself." >&2; exit 1; }
|
[ "$status" = active ] || { echo "The plugin is no longer active after updating itself." >&2; exit 1; }
|
||||||
|
|
||||||
# A zip whose top-level directory is not the slug installs a SECOND copy
|
ci_step "one copy of the plugin, not two"
|
||||||
# beside the first instead of replacing it, and the old one stays active.
|
# A zip whose top-level directory is not the slug installs a SECOND copy beside
|
||||||
count=$(docker exec "$id-cli" sh -c 'ls /var/www/html/wp-content/plugins | wc -l')
|
# the first instead of replacing it, and the old one stays active. This used to
|
||||||
|
# count the directories into a variable and then never look at it — a check
|
||||||
|
# that could not fail, sitting under a comment explaining exactly what it was
|
||||||
|
# for.
|
||||||
docker exec "$id-cli" ls /var/www/html/wp-content/plugins | sed 's/^/ /'
|
docker exec "$id-cli" ls /var/www/html/wp-content/plugins | sed 's/^/ /'
|
||||||
|
copies=$(docker exec "$id-cli" sh -c \
|
||||||
|
"ls -d /var/www/html/wp-content/plugins/$PLUGIN_SLUG* 2>/dev/null | wc -l" | tr -d '[:space:]')
|
||||||
|
echo " directories starting with $PLUGIN_SLUG: $copies"
|
||||||
|
[ "$copies" = 1 ] || {
|
||||||
|
echo "Expected exactly one, got $copies. The zip unpacked to a directory that is not the slug." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ci_step "the code that is now running reports the new version"
|
||||||
|
# Files can be replaced while the running code still reports the old version —
|
||||||
|
# an opcache that was not invalidated, or a constant defined somewhere else.
|
||||||
|
# The constant name is read out of the plugin header rather than hard-coded:
|
||||||
|
# this script is copied between plugins, and a name that does not exist over
|
||||||
|
# there prints an empty value and carries on cheerfully.
|
||||||
|
const=$(sed -n -E "s/^[[:space:]]*define\([[:space:]]*'([A-Z_]*VERSION)'.*/\1/p" "$PLUGIN_MAIN" | head -1)
|
||||||
|
if [ -n "$const" ]; then
|
||||||
|
running=$(wp eval "echo defined( '$const' ) ? $const : '';" | tr -d '[:space:]')
|
||||||
|
echo " $const = ${running:-<undefined>}"
|
||||||
|
[ "$running" = "$expected" ] || {
|
||||||
|
echo "The files say $expected but the running code says '${running:-undefined}'." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo " $PLUGIN_MAIN defines no *_VERSION constant — nothing to cross-check."
|
||||||
|
fi
|
||||||
|
|
||||||
ci_step "and it still works afterwards"
|
|
||||||
wp eval 'echo WC_EDM_VERSION;' | sed 's/^/ WC_EDM_VERSION: /'
|
|
||||||
wp plugin list --name="$PLUGIN_SLUG" --field=update | grep -qx none \
|
wp plugin list --name="$PLUGIN_SLUG" --field=update | grep -qx none \
|
||||||
&& echo " WordPress no longer offers an update — the header and the feed agree."
|
&& echo " WordPress no longer offers an update — the header and the feed agree."
|
||||||
|
|
||||||
|
|||||||
+57
-5
@@ -71,11 +71,14 @@ Three things in that file are load-bearing and none of them look it:
|
|||||||
WordPress 5.8. Remove the header and a .org plugin that happens to share the
|
WordPress 5.8. Remove the header and a .org plugin that happens to share the
|
||||||
slug could be pushed onto a shop running yours.
|
slug could be pushed onto a shop running yours.
|
||||||
|
|
||||||
The answer is cached for six hours, which is right for the check WordPress runs
|
🔴 **Keep the feed cache short — WordPress is already the rate limiter.** It
|
||||||
twice a day and wrong for the **Check again** link — so `forget_on_force_check()`
|
gates its own check by screen: one minute on Dashboard → Updates, one hour on
|
||||||
drops the cache on `force-check`. Without it, someone who has just published a
|
the Plugins screen, 2–12 hours in the background. This filter only runs when
|
||||||
release clicks that link, sees nothing, and concludes the whole mechanism is
|
that check runs, so any cache longer than that cadence simply hides new
|
||||||
broken.
|
releases underneath it. Ours was six hours "because WordPress checks twice a
|
||||||
|
day anyway", and the symptom was a release going out and the shop being shown
|
||||||
|
nothing. It is fifteen minutes now, and `forget_on_force_check()` drops it
|
||||||
|
entirely on the **Check again** link.
|
||||||
|
|
||||||
**6. Make the Gitea repository public**, or point `Update URI` at a separate
|
**6. Make the Gitea repository public**, or point `Update URI` at a separate
|
||||||
public releases-only repository. Every shop reads that feed without
|
public releases-only repository. Every shop reads that feed without
|
||||||
@@ -105,6 +108,38 @@ it cannot.
|
|||||||
- `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is
|
- `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is
|
||||||
the only way to notice that a workflow stopped running on Gitea.
|
the only way to notice that a workflow stopped running on Gitea.
|
||||||
|
|
||||||
|
## Running these locally: `git add` first
|
||||||
|
|
||||||
|
Every one of these scripts derives "what ships" from `git ls-files`, which
|
||||||
|
reads the **index**. An uncommitted file is therefore invisible to
|
||||||
|
`ci/dist-files.sh`, to the release zip, and to the copy that goes into the test
|
||||||
|
site.
|
||||||
|
|
||||||
|
That is deliberate — it is what catches a file that was written, referenced and
|
||||||
|
never committed, which is a release that is missing a file while every local
|
||||||
|
test passes because the file is sitting right there in the working tree. But it
|
||||||
|
also means running any of this before `git add` tests a release without your
|
||||||
|
newest file, and the failure is silent: the check does not complain, it just
|
||||||
|
measures something else. It cost two rounds here, once on
|
||||||
|
`includes/class-updater.php` and once on `assets/icon.svg`.
|
||||||
|
|
||||||
|
If a script reports something you just fixed as still missing, `git status`
|
||||||
|
before you debug the script.
|
||||||
|
|
||||||
|
## Make a check fail once before you believe it
|
||||||
|
|
||||||
|
Three checks in this directory turned out to be incapable of failing, and all
|
||||||
|
three sat under a comment explaining exactly what they were for:
|
||||||
|
|
||||||
|
- `tests/smoke.php` printed `FAIL` and exited 0 for four releases.
|
||||||
|
- `ci/static.sh`'s PHP lint passed while linting an empty directory.
|
||||||
|
- `ci/update-smoke.sh` counted the plugin directories into a variable and never
|
||||||
|
looked at it — under a comment about a zip installing a second copy.
|
||||||
|
|
||||||
|
None of them were caught by reading the code, and a green build is exactly what
|
||||||
|
a broken check looks like. So: break it on purpose once, watch it go red, then
|
||||||
|
put it back. It costs one run.
|
||||||
|
|
||||||
## Two guards worth understanding before you move them
|
## Two guards worth understanding before you move them
|
||||||
|
|
||||||
**`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the
|
**`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the
|
||||||
@@ -118,6 +153,23 @@ and a release with no zip attached both produce a page that looks perfect to
|
|||||||
anyone logged in, and an update that no shop is ever offered. A check that
|
anyone logged in, and an update that no shop is ever offered. A check that
|
||||||
sends a token is the false positive that hides exactly that.
|
sends a token is the false positive that hides exactly that.
|
||||||
|
|
||||||
|
**Do not parse a forge's JSON with `sed`.** A greedy `.*"id":([0-9]+)` matches
|
||||||
|
the *last* id in the payload, and a release object carries an embedded
|
||||||
|
`author` and, once anything is attached, an `assets` array — each with an id of
|
||||||
|
its own. `ci/publish-release.sh` uses a four-line `json_field()` over `python3`
|
||||||
|
instead. Measured here: the greedy pattern returns the asset ids 1/2/3/4 for
|
||||||
|
releases whose real ids are 416/417/419/421.
|
||||||
|
|
||||||
|
Four releases were published correctly anyway, and the reason is **who
|
||||||
|
published them, not which instance**: a release created by the Actions job
|
||||||
|
token has the ghost user as its author (`"id":-1`), and the minus sign stops
|
||||||
|
`[0-9]+` from matching. A release created by a real user — a cross-repo PAT, or
|
||||||
|
one published by hand — has `"id":1` and the greedy match lands on it. Do not
|
||||||
|
read the `-1` as a safety margin; it lasts until the first release somebody
|
||||||
|
publishes by hand. And on a re-run the lookup fetches a response that *has*
|
||||||
|
assets, where the last id is an asset id: positive, on any instance, with any
|
||||||
|
token.
|
||||||
|
|
||||||
**A forge's own download URL is not the address your users can reach.**
|
**A forge's own download URL is not the address your users can reach.**
|
||||||
`browser_download_url` is built from the forge's configured root URL;
|
`browser_download_url` is built from the forge's configured root URL;
|
||||||
`gitea.nolimit.dk` reports `http://57.129.89.117:25001/...` there. The updater
|
`gitea.nolimit.dk` reports `http://57.129.89.117:25001/...` there. The updater
|
||||||
|
|||||||
@@ -185,11 +185,21 @@ the real list table and asserts the flag rather than trusting the reasoning.
|
|||||||
|
|
||||||
### "Check again" really checks again
|
### "Check again" really checks again
|
||||||
|
|
||||||
The feed is cached for six hours. That is right for the check WordPress runs on
|
The feed is cached for fifteen minutes, and the cache is dropped entirely when
|
||||||
its own twice a day, and wrong for a link whose entire promise is that it looks
|
wp-admin is loaded with `force-check`.
|
||||||
*now* — so the cache is dropped when wp-admin is loaded with `force-check`.
|
|
||||||
Without that, publishing a release and then clicking **Check again** shows
|
It was six hours, on the reasoning that WordPress checks twice a day anyway.
|
||||||
nothing for up to six hours, which reads exactly like a broken updater.
|
That reasoning was wrong. WordPress gates its own check **by screen**:
|
||||||
|
|
||||||
|
| Screen | How often WordPress re-checks |
|
||||||
|
| --- | --- |
|
||||||
|
| Dashboard → Updates | every minute |
|
||||||
|
| Plugins | every hour |
|
||||||
|
| Background / cron | every 2–12 hours |
|
||||||
|
|
||||||
|
This filter only runs when that check runs, so WordPress is already the rate
|
||||||
|
limiter. A cache longer than its cadence does nothing but hide new releases
|
||||||
|
underneath it — a release published, and a shop shown nothing.
|
||||||
|
|
||||||
From the command line, `wp transient delete wc_edm_release_feed` does the same.
|
From the command line, `wp transient delete wc_edm_release_feed` does the same.
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,24 @@ class WC_EDM_Updater {
|
|||||||
const TRANSIENT = 'wc_edm_release_feed';
|
const TRANSIENT = 'wc_edm_release_feed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long that cache lives. WordPress checks for updates twice a day; a
|
* How long that cache lives.
|
||||||
* shorter window would only add requests, a longer one would delay a fix.
|
*
|
||||||
|
* Six hours, originally, on the reasoning that WordPress checks twice a
|
||||||
|
* day anyway. That reasoning was wrong, and the symptom was a shop that
|
||||||
|
* had just been told a new version existed and was shown nothing.
|
||||||
|
*
|
||||||
|
* WordPress does not check twice a day when somebody is looking. It gates
|
||||||
|
* its own check by screen — one minute on Dashboard → Updates, one hour on
|
||||||
|
* the Plugins screen, twelve hours in the background (wp_update_plugins(),
|
||||||
|
* switch on current_filter()). This filter only runs when that check runs,
|
||||||
|
* so WordPress is already the rate limiter, and a cache longer than its
|
||||||
|
* cadence does nothing but hide new releases underneath it.
|
||||||
|
*
|
||||||
|
* Fifteen minutes is left only to absorb a burst — several calls in one
|
||||||
|
* request, or other code calling wp_update_plugins() with extra stats,
|
||||||
|
* which bypasses the gate above.
|
||||||
*/
|
*/
|
||||||
const TTL = 6 * HOUR_IN_SECONDS;
|
const TTL = 15 * MINUTE_IN_SECONDS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook into the update check for our own host only.
|
* Hook into the update check for our own host only.
|
||||||
@@ -49,7 +63,7 @@ class WC_EDM_Updater {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
add_filter( "update_plugins_{$host}", array( $this, 'offer_update' ), 10, 2 );
|
add_filter( "update_plugins_{$host}", array( $this, 'offer_update' ), 10, 3 );
|
||||||
|
|
||||||
// Without this the old answer is served for up to six hours after an
|
// Without this the old answer is served for up to six hours after an
|
||||||
// update, and the plugins screen keeps offering a version already
|
// update, and the plugins screen keeps offering a version already
|
||||||
@@ -58,6 +72,9 @@ class WC_EDM_Updater {
|
|||||||
|
|
||||||
// And "Check again" has to actually check again.
|
// And "Check again" has to actually check again.
|
||||||
add_action( 'admin_init', array( $this, 'forget_on_force_check' ) );
|
add_action( 'admin_init', array( $this, 'forget_on_force_check' ) );
|
||||||
|
|
||||||
|
// "View version details" next to the update notice.
|
||||||
|
add_filter( 'plugins_api', array( $this, 'plugin_details' ), 10, 3 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,9 +126,26 @@ class WC_EDM_Updater {
|
|||||||
*
|
*
|
||||||
* @param array|false $update Update offered so far.
|
* @param array|false $update Update offered so far.
|
||||||
* @param array $plugin_data Headers of the installed plugin.
|
* @param array $plugin_data Headers of the installed plugin.
|
||||||
|
* @param string $plugin_file Which plugin WordPress is asking about.
|
||||||
* @return array|false
|
* @return array|false
|
||||||
*/
|
*/
|
||||||
public function offer_update( $update, $plugin_data ) {
|
public function offer_update( $update, $plugin_data, $plugin_file = '' ) {
|
||||||
|
// The filter name is built from the HOSTNAME, not the repository:
|
||||||
|
// every plugin that updates from gitea.nolimit.dk shares the one hook
|
||||||
|
// `update_plugins_gitea.nolimit.dk`, and WordPress runs every callback
|
||||||
|
// on it for every one of those plugins. Without this check a plugin
|
||||||
|
// answers for its neighbour and offers it its own release — measured
|
||||||
|
// on a live shop, where a subscriptions plugin was offered this one's
|
||||||
|
// 1.4.4 and told it was an update for itself.
|
||||||
|
//
|
||||||
|
// The `10, 3` on the add_filter above is half of this fix. With the
|
||||||
|
// accepted-argument count left at 2 the third argument never arrives,
|
||||||
|
// $plugin_file stays empty, and the guard silently does nothing — the
|
||||||
|
// same shape of bug it exists to fix.
|
||||||
|
if ( '' !== $plugin_file && plugin_basename( WC_EDM_PLUGIN_FILE ) !== $plugin_file ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
$release = $this->latest_release();
|
$release = $this->latest_release();
|
||||||
|
|
||||||
if ( ! $release ) {
|
if ( ! $release ) {
|
||||||
@@ -141,6 +175,58 @@ class WC_EDM_Updater {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answer the "View version details" modal.
|
||||||
|
*
|
||||||
|
* WordPress builds that link as
|
||||||
|
* plugin-install.php?tab=plugin-information&plugin=<slug>, which calls
|
||||||
|
* plugins_api() — and plugins_api() asks **wordpress.org**. This plugin is
|
||||||
|
* not there, so without this filter the link opens a modal containing an
|
||||||
|
* error, on the same screen that is otherwise telling the shop owner an
|
||||||
|
* update is ready. Nothing warns you: the link looks like every other
|
||||||
|
* plugin's.
|
||||||
|
*
|
||||||
|
* @param false|object|array $result Response so far.
|
||||||
|
* @param string $action Requested action.
|
||||||
|
* @param object $args Request arguments.
|
||||||
|
* @return false|object|array
|
||||||
|
*/
|
||||||
|
public function plugin_details( $result, $action, $args ) {
|
||||||
|
$slug = dirname( plugin_basename( WC_EDM_PLUGIN_FILE ) );
|
||||||
|
|
||||||
|
if ( 'plugin_information' !== $action || empty( $args->slug ) || $args->slug !== $slug ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
$plugin = get_plugin_data( WC_EDM_PLUGIN_FILE, false, false );
|
||||||
|
$release = $this->latest_release();
|
||||||
|
|
||||||
|
$notes = $release && '' !== $release['notes'] ? $release['notes'] : '';
|
||||||
|
|
||||||
|
return (object) array(
|
||||||
|
'name' => $plugin['Name'],
|
||||||
|
'slug' => $slug,
|
||||||
|
'version' => $release ? $release['version'] : $plugin['Version'],
|
||||||
|
'author' => $plugin['Author'],
|
||||||
|
'author_profile'=> $plugin['AuthorURI'],
|
||||||
|
'homepage' => $plugin['PluginURI'],
|
||||||
|
'requires' => $plugin['RequiresWP'],
|
||||||
|
'requires_php' => $plugin['RequiresPHP'],
|
||||||
|
'tested' => $release ? $release['tested'] : '',
|
||||||
|
'download_link' => $release ? $release['package'] : '',
|
||||||
|
'icons' => array( 'svg' => WC_EDM_PLUGIN_URL . 'assets/icon.svg' ),
|
||||||
|
'sections' => array(
|
||||||
|
'description' => wpautop( esc_html( $plugin['Description'] ) ),
|
||||||
|
// Plain text from the release notes, not markdown: rendering it
|
||||||
|
// as HTML would mean trusting whatever is in a release body.
|
||||||
|
'changelog' => '' !== $notes
|
||||||
|
? wpautop( esc_html( $notes ) )
|
||||||
|
: wpautop( esc_html__( 'Release notes are published with each release.', 'wc-email-discount' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop the cached feed.
|
* Drop the cached feed.
|
||||||
*/
|
*/
|
||||||
@@ -274,6 +360,9 @@ class WC_EDM_Updater {
|
|||||||
'package' => $base . '/releases/download/' . rawurlencode( $tag ) . '/' . rawurlencode( $zip ),
|
'package' => $base . '/releases/download/' . rawurlencode( $tag ) . '/' . rawurlencode( $zip ),
|
||||||
'url' => $base . '/releases/tag/' . rawurlencode( $tag ),
|
'url' => $base . '/releases/tag/' . rawurlencode( $tag ),
|
||||||
'tested' => $tested,
|
'tested' => $tested,
|
||||||
|
// The release notes are the changelog. Kept so the "View version
|
||||||
|
// details" modal has something true to show.
|
||||||
|
'notes' => isset( $body['body'] ) ? (string) $body['body'] : '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+125
-9
@@ -14,7 +14,29 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$wc_edm_failures = 0;
|
/*
|
||||||
|
* The counter lives in $GLOBALS on purpose, and the four lines below prove it
|
||||||
|
* reaches the verdict before anything relies on it.
|
||||||
|
*
|
||||||
|
* `wp eval-file` runs this file through eval() INSIDE A METHOD, so the top
|
||||||
|
* level here is function scope, not global scope. A plain `$failures = 0` at
|
||||||
|
* the top and a `global $failures` in a helper below are therefore two
|
||||||
|
* different variables: the helpers increment the real global, the verdict at
|
||||||
|
* the bottom reads the local, and the local is always zero. Every assertion
|
||||||
|
* fails, every FAIL line prints, the file ends with "All assertions passed"
|
||||||
|
* and exits 0 — and CI goes green. That is not reasoning, it is what this file
|
||||||
|
* did until it was measured.
|
||||||
|
*
|
||||||
|
* This is the one file whose entire job is to be able to go red, so it checks
|
||||||
|
* that it can.
|
||||||
|
*/
|
||||||
|
$GLOBALS['wc_edm_failures'] = 0;
|
||||||
|
( static function () { ++$GLOBALS['wc_edm_failures']; } )();
|
||||||
|
if ( 1 !== $GLOBALS['wc_edm_failures'] ) {
|
||||||
|
echo "The failure counter does not reach the verdict: this file cannot go red.\n";
|
||||||
|
exit( 1 );
|
||||||
|
}
|
||||||
|
$GLOBALS['wc_edm_failures'] = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert a condition and keep going, so one run reports every failure.
|
* Assert a condition and keep going, so one run reports every failure.
|
||||||
@@ -24,14 +46,12 @@ $wc_edm_failures = 0;
|
|||||||
* @param string $detail Shown when it fails.
|
* @param string $detail Shown when it fails.
|
||||||
*/
|
*/
|
||||||
function wc_edm_ok( $what, $condition, $detail = '' ) {
|
function wc_edm_ok( $what, $condition, $detail = '' ) {
|
||||||
global $wc_edm_failures;
|
|
||||||
|
|
||||||
if ( $condition ) {
|
if ( $condition ) {
|
||||||
printf( " ok %s\n", $what );
|
printf( " ok %s\n", $what );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
++$wc_edm_failures;
|
++$GLOBALS['wc_edm_failures'];
|
||||||
printf( " FAIL %s%s\n", $what, '' !== $detail ? " — {$detail}" : '' );
|
printf( " FAIL %s%s\n", $what, '' !== $detail ? " — {$detail}" : '' );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +305,7 @@ set_transient(
|
|||||||
60
|
60
|
||||||
);
|
);
|
||||||
|
|
||||||
$offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ) );
|
$offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ), plugin_basename( WC_EDM_PLUGIN_FILE ) );
|
||||||
wc_edm_is( 'a newer release is offered as an update', '99.0.0', is_array( $offered ) ? $offered['version'] : null );
|
wc_edm_is( 'a newer release is offered as an update', '99.0.0', is_array( $offered ) ? $offered['version'] : null );
|
||||||
wc_edm_is(
|
wc_edm_is(
|
||||||
'and it carries the folder name WordPress will unpack into',
|
'and it carries the folder name WordPress will unpack into',
|
||||||
@@ -324,9 +344,79 @@ set_transient(
|
|||||||
),
|
),
|
||||||
60
|
60
|
||||||
);
|
);
|
||||||
$offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ) );
|
$offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ), plugin_basename( WC_EDM_PLUGIN_FILE ) );
|
||||||
wc_edm_is( 'the current version is answered too, not passed over', WC_EDM_VERSION, is_array( $offered ) ? $offered['version'] : null );
|
wc_edm_is( 'the current version is answered too, not passed over', WC_EDM_VERSION, is_array( $offered ) ? $offered['version'] : null );
|
||||||
|
|
||||||
|
// 🔴 The filter name is built from the HOSTNAME, not the repository. Every
|
||||||
|
// plugin updating from gitea.nolimit.dk shares the hook
|
||||||
|
// update_plugins_gitea.nolimit.dk, and WordPress runs every callback on it for
|
||||||
|
// every one of those plugins. Answering unconditionally means offering the
|
||||||
|
// neighbour your own release — which happened on a live shop: a subscriptions
|
||||||
|
// plugin was shown this one's 1.4.4 and told it was an update for itself. Load
|
||||||
|
// order decides who wins, so the direction it breaks in is arbitrary.
|
||||||
|
//
|
||||||
|
// The site test installs exactly one plugin from this host, so the collision
|
||||||
|
// can never occur here. This assertion is the whole defence.
|
||||||
|
wc_edm_ok(
|
||||||
|
'another plugin on the same host is not offered our release',
|
||||||
|
false === wc_edm()->updater->offer_update(
|
||||||
|
false,
|
||||||
|
get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ),
|
||||||
|
'kw-subscriptions/kw-subscriptions.php'
|
||||||
|
),
|
||||||
|
'we answered for a plugin that is not ours'
|
||||||
|
);
|
||||||
|
|
||||||
|
// The "View version details" link next to the update notice. WordPress builds
|
||||||
|
// it as plugin-install.php?tab=plugin-information&plugin=<slug>, which calls
|
||||||
|
// plugins_api() — and plugins_api() asks wordpress.org, where this plugin does
|
||||||
|
// not exist. Unanswered, that link opens a modal reading "Plugin not found."
|
||||||
|
// on the same screen that is telling the shop owner an update is ready.
|
||||||
|
// Measured on a live shop before this was written.
|
||||||
|
set_transient(
|
||||||
|
WC_EDM_Updater::TRANSIENT,
|
||||||
|
array(
|
||||||
|
'version' => '99.0.0',
|
||||||
|
'package' => 'https://gitea.nolimit.dk/kw/demo/releases/download/v99.0.0/demo.zip',
|
||||||
|
'url' => 'https://gitea.nolimit.dk/kw/demo',
|
||||||
|
'tested' => '7.1',
|
||||||
|
'notes' => "v99.0.0 — a headline\n\nA line of release notes.",
|
||||||
|
),
|
||||||
|
600
|
||||||
|
);
|
||||||
|
|
||||||
|
// plugins_api() lives in the admin includes, which a WP-CLI context does not
|
||||||
|
// load. wp-admin has it; this does not.
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
|
||||||
|
|
||||||
|
$wc_edm_slug = dirname( plugin_basename( WC_EDM_PLUGIN_FILE ) );
|
||||||
|
$details = plugins_api( 'plugin_information', array( 'slug' => $wc_edm_slug ) );
|
||||||
|
|
||||||
|
wc_edm_ok(
|
||||||
|
'the details modal is answered here, not by wordpress.org',
|
||||||
|
! is_wp_error( $details ),
|
||||||
|
is_wp_error( $details ) ? $details->get_error_message() : ''
|
||||||
|
);
|
||||||
|
wc_edm_is( 'and it is about this plugin', $wc_edm_slug, is_wp_error( $details ) ? null : $details->slug );
|
||||||
|
wc_edm_is( 'it reports the version from the feed', '99.0.0', is_wp_error( $details ) ? null : $details->version );
|
||||||
|
wc_edm_ok(
|
||||||
|
'the changelog is the release notes',
|
||||||
|
! is_wp_error( $details ) && false !== strpos( $details->sections['changelog'], 'A line of release notes' ),
|
||||||
|
'a modal with an empty changelog is barely better than an error'
|
||||||
|
);
|
||||||
|
wc_edm_ok(
|
||||||
|
'and the notes are escaped rather than rendered',
|
||||||
|
! is_wp_error( $details ) && false === strpos( $details->sections['changelog'], '<script' ),
|
||||||
|
'a release body is not a template language'
|
||||||
|
);
|
||||||
|
wc_edm_ok(
|
||||||
|
'another plugin\'s details request is left alone',
|
||||||
|
false === wc_edm()->updater->plugin_details( false, 'plugin_information', (object) array( 'slug' => 'akismet' ) ),
|
||||||
|
'answering for somebody else would replace their modal with ours'
|
||||||
|
);
|
||||||
|
|
||||||
|
delete_transient( WC_EDM_Updater::TRANSIENT );
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The "Enable auto-updates" link on the Plugins screen.
|
* The "Enable auto-updates" link on the Plugins screen.
|
||||||
*
|
*
|
||||||
@@ -345,7 +435,18 @@ wc_edm_is( 'the current version is answered too, not passed over', WC_EDM_VERSIO
|
|||||||
* WordPress does with our answer, and a test that needs the internet fails for
|
* WordPress does with our answer, and a test that needs the internet fails for
|
||||||
* reasons that have nothing to do with it.
|
* reasons that have nothing to do with it.
|
||||||
*/
|
*/
|
||||||
// "Check again" must discard the six-hour cache, or the button lies: someone
|
// The cache must not outlive WordPress's own check cadence. It gates by screen
|
||||||
|
// — one minute on Dashboard → Updates, one hour on the Plugins screen — and
|
||||||
|
// this filter only runs when that check runs, so anything longer here hides a
|
||||||
|
// new release underneath a cache the shop owner cannot see. This was six hours
|
||||||
|
// and produced exactly that: a release published, and a shop shown nothing.
|
||||||
|
wc_edm_ok(
|
||||||
|
'the cache is shorter than the interval WordPress re-checks on',
|
||||||
|
WC_EDM_Updater::TTL <= 15 * MINUTE_IN_SECONDS,
|
||||||
|
'TTL is ' . WC_EDM_Updater::TTL . 's; WordPress re-checks every 60s on Dashboard → Updates'
|
||||||
|
);
|
||||||
|
|
||||||
|
// "Check again" must discard the cache, or the button lies: someone
|
||||||
// who has just published a release clicks it, sees nothing, and concludes the
|
// who has just published a release clicks it, sees nothing, and concludes the
|
||||||
// whole mechanism is broken.
|
// whole mechanism is broken.
|
||||||
set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => '1.0.0', 'package' => 'x', 'url' => 'x', 'tested' => '' ), 600 );
|
set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => '1.0.0', 'package' => 'x', 'url' => 'x', 'tested' => '' ), 600 );
|
||||||
@@ -368,6 +469,21 @@ delete_transient( WC_EDM_Updater::TRANSIENT );
|
|||||||
// strlen(null) deprecation from class-wp-plugins-list-table.php. It is
|
// strlen(null) deprecation from class-wp-plugins-list-table.php. It is
|
||||||
// WordPress's, not ours, which is why the deprecation check filters on this
|
// WordPress's, not ours, which is why the deprecation check filters on this
|
||||||
// plugin's directory rather than failing on everything it sees.
|
// plugin's directory rather than failing on everything it sees.
|
||||||
|
// Primed again deliberately. The force-check assertions above deleted it, and
|
||||||
|
// without this offer_update() makes a real HTTP request — which passes only
|
||||||
|
// because this plugin's feed repo happens to be public. That would make the
|
||||||
|
// assertion measure the environment rather than the code under test.
|
||||||
|
set_transient(
|
||||||
|
WC_EDM_Updater::TRANSIENT,
|
||||||
|
array(
|
||||||
|
'version' => WC_EDM_VERSION,
|
||||||
|
'package' => 'https://gitea.nolimit.dk/kw/demo/releases/download/v1/demo.zip',
|
||||||
|
'url' => 'https://gitea.nolimit.dk/kw/demo',
|
||||||
|
'tested' => '7.1',
|
||||||
|
),
|
||||||
|
600
|
||||||
|
);
|
||||||
|
|
||||||
delete_site_transient( 'update_plugins' );
|
delete_site_transient( 'update_plugins' );
|
||||||
wp_update_plugins();
|
wp_update_plugins();
|
||||||
$transient = get_site_transient( 'update_plugins' );
|
$transient = get_site_transient( 'update_plugins' );
|
||||||
@@ -407,8 +523,8 @@ delete_transient( WC_EDM_Updater::TRANSIENT );
|
|||||||
/* --------------------------------------------------------------- verdict -- */
|
/* --------------------------------------------------------------- verdict -- */
|
||||||
|
|
||||||
echo "\n";
|
echo "\n";
|
||||||
if ( $wc_edm_failures > 0 ) {
|
if ( $GLOBALS['wc_edm_failures'] > 0 ) {
|
||||||
printf( "%d assertion(s) failed on WordPress %s.\n", $wc_edm_failures, get_bloginfo( 'version' ) );
|
printf( "%d assertion(s) failed on WordPress %s.\n", $GLOBALS['wc_edm_failures'], get_bloginfo( 'version' ) );
|
||||||
exit( 1 );
|
exit( 1 );
|
||||||
}
|
}
|
||||||
printf(
|
printf(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Plugin URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
|
* Plugin URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
|
||||||
* Update URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
|
* Update URI: https://gitea.nolimit.dk/kw/woocommerce-email-discount-manager
|
||||||
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
||||||
* Version: 1.4.3
|
* Version: 1.4.4
|
||||||
* Author: Kristian Wind
|
* Author: Kristian Wind
|
||||||
* Author URI: https://gitea.nolimit.dk/kw
|
* Author URI: https://gitea.nolimit.dk/kw
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
define( 'WC_EDM_VERSION', '1.4.3' );
|
define( 'WC_EDM_VERSION', '1.4.4' );
|
||||||
define( 'WC_EDM_PLUGIN_FILE', __FILE__ );
|
define( 'WC_EDM_PLUGIN_FILE', __FILE__ );
|
||||||
define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'WC_EDM_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
define( 'WC_EDM_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user