Fix resolving missing normalised pages

And fix some UI stuff. Sorry, this took me so long I mostly forgot what I did halfway through.
This commit is contained in:
Florine W. Dekker 2022-12-16 22:40:43 +01:00
parent 83fb8958ad
commit 75cf6e4e77
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
12 changed files with 348 additions and 231 deletions

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "0.19.9", "_comment_version": "Also update version in `package.json`!",
"version": "0.19.10", "_comment_version": "Also update version in `package.json`!",
"type": "project",
"license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier",

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.19.9", "_comment_version": "Also update version in `composer.json`!",
"version": "0.19.10", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -85,8 +85,8 @@
</hgroup>
</header>
<div class="article-contents">
<form id="login-form" novalidate>
<article class="status-card hidden" data-status-for="login-form">
<form id="login-form" data-status-card="login-status-card" novalidate>
<article id="login-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -126,8 +126,8 @@
</hgroup>
</header>
<div class="article-contents">
<form id="register-form" novalidate>
<article class="status-card hidden" data-status-for="register-form">
<form id="register-form" data-status-card="register-status-card" novalidate>
<article id="register-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -142,8 +142,7 @@
<small id="register-password-hint" data-hint-for="register-password"
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="register-password-toggle"
class="password-toggle"
<input type="checkbox" role="switch" id="register-password-toggle" class="password-toggle"
data-toggles="register-password" />
<label for="register-password-toggle">Show password</label>
<br /><br />
@ -162,8 +161,9 @@
</hgroup>
</header>
<div class="article-contents">
<form id="send-password-reset-form" novalidate>
<article class="status-card hidden" data-status-for="send-password-reset-form">
<form id="send-password-reset-form" data-status-card="send-password-reset-status-card"
novalidate>
<article id="send-password-reset-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -189,8 +189,8 @@
</hgroup>
</header>
<form id="reset-password-form" novalidate>
<article class="status-card hidden" data-status-for="reset-password-form">
<form id="reset-password-form" data-status-card="reset-password-status-card" novalidate>
<article id="reset-password-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -207,8 +207,7 @@
<small id="reset-password-password-hint" data-hint-for="reset-password-password"
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="reset-password-password-toggle"
class="password-toggle"
<input type="checkbox" role="switch" id="reset-password-password-toggle" class="password-toggle"
data-toggles="reset-password-password" />
<label for="reset-password-password-toggle">Show password</label>
<br /><br />
@ -224,16 +223,12 @@
<header>
<h2>Tracked articles</h2>
</header>
<article class="status-card hidden" id="remove-trackings-status-card">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<article class="status-card hidden" data-status-for="add-tracking-form">
<article id="trackings-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<div class="grid">
<form id="add-tracking-form" novalidate>
<form id="add-tracking-form" data-status-card="trackings-status-card" novalidate>
<div class="flex-form">
<!--suppress HtmlFormInputWithoutLabel -->
<input id="add-tracking-name" class="flex-form-grow" type="text" name="person_name"
@ -243,7 +238,7 @@
<small id="add-tracking-name-hint" class="input-hint" data-hint-for="add-tracking-name"></small>
</form>
<form id="filter-trackings-form" novalidate>
<form id="filter-trackings-form" data-status-card="trackings-status-card" novalidate>
<!--suppress HtmlFormInputWithoutLabel -->
<input id="filter-trackings-query" type="search" name="query" placeholder="Search" />
</form>
@ -282,8 +277,8 @@
</h4>
</hgroup>
</header>
<form id="update-email-form" novalidate>
<article class="status-card hidden" data-status-for="update-email-form">
<form id="update-email-form" data-status-card="update-email-status-card" novalidate>
<article id="update-email-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -295,8 +290,9 @@
<button id="updateEmailButton">Change email</button>
</form>
<footer>
<form id="resend-email-verification-form" novalidate>
<article class="status-card hidden" data-status-for="resend-email-verification-form">
<form id="resend-email-verification-form" data-status-card="resend-email-status-card"
novalidate>
<article id="resend-email-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -308,14 +304,16 @@
resend link
</button>
</form>
<form id="toggle-notifications-form" novalidate>
<article class="status-card hidden" data-status-for="toggle-notifications-form">
<form id="toggle-notifications-form" data-status-card="toggle-notifications-status-card"
novalidate>
<article id="toggle-notifications-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<input type="checkbox" id="notifications-enabled-checkbox" />
<label for="notifications-enabled-checkbox">Notifications</label>
<br />
<small id="notifications-enabled-checkbox-hint"
data-hint-for="notifications-enabled-checkbox"></small>
</form>
@ -326,8 +324,8 @@
<header>
<h3>Password</h3>
</header>
<form id="update-password-form" novalidate>
<article class="status-card hidden" data-status-for="update-password-form">
<form id="update-password-form" data-status-card="update-password-status-card" novalidate>
<article id="update-password-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -343,8 +341,7 @@
data-hint-for="update-password-password-old"></small>
<input type="checkbox" role="switch" id="update-password-password-old-toggle"
class="password-toggle"
data-toggles="update-password-password-old" />
class="password-toggle" data-toggles="update-password-password-old" />
<label for="update-password-password-old-toggle">Show password</label>
<br /><br />
@ -355,19 +352,21 @@
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="update-password-password-new-toggle"
class="password-toggle"
data-toggles="update-password-password-new" />
class="password-toggle" data-toggles="update-password-password-new" />
<label for="update-password-password-new-toggle">Show password</label>
<br /><br />
<button id="update-password-button">Change password</button>
</form>
<form id="forgot-password-after-login-form" novalidate>
<article class="status-card hidden" data-status-for="forgot-password-after-login-form">
<form id="forgot-password-after-login-form"
data-status-card="forgot-password-after-login-status-card" novalidate>
<article id="forgot-password-after-login-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<input id="forgot-password-after-login-email" type="hidden" name="email" />
<a role="button" id="forgot-password-after-login-button" class="outline" href="#">
Forgot password?
</a>
@ -381,8 +380,8 @@
<h4>Log out or delete your account.</h4>
</hgroup>
</header>
<form id="logout-form" novalidate>
<article class="status-card hidden" data-status-for="logout-form">
<form id="logout-form" data-status-card="logout-status-card" novalidate>
<article id="logout-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
@ -396,14 +395,25 @@
If you no longer want to use Death Notifier, you can permanently delete your account.
This choice is permanent and cannot be reverted.
</p>
<form id="delete-form" novalidate>
<article class="status-card hidden" data-status-for="delete-form">
<form id="delete-account-form" data-status-card="delete-status-card" novalidate>
<article id="delete-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<input id="delete-email" type="hidden" name="email" />
<button id="delete-button" class="outline">Delete account</button>
<input id="delete-account-actual-email" type="hidden" name="email" />
<label for="delete-account-email">Confirm email</label>
<input id="delete-account-email" name="email" autocomplete="off" />
<small id="delete-account-email-hint" data-hint-for="delete-account-email"></small>
<input type="checkbox" role="switch" id="delete-account-confirm" />
<label for="delete-account-confirm">I want to permanently delete my account</label>
<br />
<small id="delete-account-confirm-hint" data-hint-for="delete-account-confirm"></small>
<br /><br />
<button id="delete-account-button" class="outline">Delete account</button>
</form>
</footer>
</article>

View File

@ -1,8 +1,8 @@
// @ts-ignore
const {$, $a, doAfterLoad} = window.fwdekker;
const {
clearFormValidity, clearMessageStatus, showInputValid, showMessageInfo, showMessageError, showMessageSuccess,
showMessageWarning
clearFormValidity, clearMessageStatus, showInputInvalid, showInputValid, showMessageInfo, showMessageError,
showMessageSuccess, showMessageWarning
// @ts-ignore
} = window.fwdekker.validation;
@ -35,7 +35,7 @@ function createPlaceholderRow(text: string): HTMLTableRowElement {
cell.innerText = text;
row.appendChild(cell);
// Is a button to give exact same height as non-placeholder rows
// A button to give exact same height as non-placeholder rows
const spacerCell = document.createElement("td");
const spacer = document.createElement("button");
spacer.ariaHidden = "true";
@ -54,7 +54,7 @@ function createPlaceholderRow(text: string): HTMLTableRowElement {
function refreshTrackings(): void {
getApi(
{action: "list-trackings", token: csrfToken ?? ""},
sharedMessageElement,
$("#trackings-status-card"),
(response) => {
// Remove old rows
$a("#trackings tbody tr").forEach((it: HTMLTableRowElement) => it.remove());
@ -110,7 +110,7 @@ function refreshTrackings(): void {
() => {
refreshTrackings();
showMessageSuccess(
$("#remove-trackings-status-card"),
$("#trackings-status-card"),
`Successfully removed <b>${tracking.name}</b>.`
);
}
@ -146,7 +146,7 @@ function refreshUserData(): void {
const userData = response.payload;
// Account deletion
$("#delete-email").value = userData.email;
$("#delete-account-actual-email").value = userData.email;
// Email
$("#update-email-email").value = userData.email;
@ -320,7 +320,7 @@ doAfterLoad(() => {
});
// Login
// Login/register
const loginForm = $("#login-form");
loginForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -384,7 +384,7 @@ doAfterLoad(() => {
});
// Forgot password
// Forgot/reset password
const sendPasswordResetForm = $("#send-password-reset-form");
sendPasswordResetForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -460,163 +460,6 @@ doAfterLoad(() => {
});
// Account management
const deleteForm = $("#delete-form");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const actual_email = $("#delete-email").value;
// TODO: Replace with modal dialog
const entered_email = window.prompt(
`Are you sure you want to delete your account? ` +
`This action cannot be undone. ` +
`Enter ${actual_email} to confirm.`
);
if (entered_email === null) {
return;
} else if (entered_email !== actual_email) {
showMessageError(deleteForm, "Incorrect email address.");
return;
}
postApi(
{action: "user-delete", token: csrfToken},
deleteForm,
() => {
logoutHandler.invokeListeners();
showMessageSuccess(sharedMessageElement, "Your account has been deleted.");
}
);
});
logoutHandler.addListener(() => clearFormValidity(deleteForm));
const resendEmailVerificationForm = $("#resend-email-verification-form");
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "resend-verify-email", token: csrfToken},
resendEmailVerificationForm,
() => {
refreshUserData();
showMessageSuccess(
resendEmailVerificationForm,
"An email with verification instructions has been sent and should arrive within a few minutes."
);
}
);
});
logoutHandler.addListener(() => clearFormValidity(resendEmailVerificationForm));
const toggleNotificationsForm = $("#toggle-notifications-form");
const notificationsEnabledCheckbox = $("#notifications-enabled-checkbox");
notificationsEnabledCheckbox.addEventListener("change", (event: Event) => {
event.preventDefault();
const enableNotifications = notificationsEnabledCheckbox.checked;
postApi(
{
action: "toggle-notifications",
enable_notifications: enableNotifications,
token: csrfToken
},
toggleNotificationsForm,
() => {
refreshUserData();
if (enableNotifications)
showInputValid($("#notifications-enabled-checkbox"), "Notifications have been enabled.");
else
showInputValid(
$("#notifications-enabled-checkbox"),
"Notifications have been disabled. " +
"You will still receive security notifications, for example if you change your email address " +
"or password."
);
}
);
});
logoutHandler.addListener(() => clearFormValidity(toggleNotificationsForm));
const updateEmailForm = $("#update-email-form");
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-email",
token: csrfToken,
email: $("#update-email-email").value,
},
updateEmailForm,
() => {
updateEmailForm.reset();
refreshUserData();
showMessageSuccess(
updateEmailForm,
"Email address updated successfully! " +
"An email with verification instructions has been sent and should arrive within a few minutes. " +
"You will not receive notifications until you verify your email address."
);
}
);
});
logoutHandler.addListener(() => {
updateEmailForm.reset();
clearFormValidity(updateEmailForm);
});
const updatePasswordForm = $("#update-password-form");
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-password",
token: csrfToken,
password_old: $("#update-password-password-old").value,
password_new: $("#update-password-password-new").value,
},
event.target as HTMLFormElement,
() => {
updatePasswordForm.reset();
refreshUserData();
showMessageSuccess(updatePasswordForm, "Password updated successfully!");
}
);
});
logoutHandler.addListener(() => {
updatePasswordForm.reset();
clearFormValidity(updatePasswordForm);
});
const forgotPasswordAfterLoginForm = $("#forgot-password-after-login-form");
$("#forgot-password-after-login-button").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
if (!window.confirm("Send an email to help reset your password?")) return;
postApi(
{
action: "send-password-reset",
token: csrfToken,
email: $("#forgot-password-after-login-email").value,
},
forgotPasswordAfterLoginForm,
() => {
showMessageSuccess(
forgotPasswordAfterLoginForm,
"An email with password reset instructions has been sent and should arrive within a few minutes."
);
}
);
});
logoutHandler.addListener(() => {
forgotPasswordAfterLoginForm.reset();
clearFormValidity(forgotPasswordAfterLoginForm);
});
// Tracking management
const queryInput = $("#filter-trackings-query");
$("#filter-trackings-form").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
@ -675,6 +518,172 @@ doAfterLoad(() => {
addTrackingForm.reset();
clearFormValidity(addTrackingForm);
});
// Account management
const updateEmailForm = $("#update-email-form");
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-email",
token: csrfToken,
email: $("#update-email-email").value,
},
updateEmailForm,
() => {
updateEmailForm.reset();
refreshUserData();
showMessageSuccess(
updateEmailForm,
"Email address updated successfully! " +
"An email with verification instructions has been sent and should arrive within a few minutes. " +
"You will not receive notifications until you verify your email address."
);
}
);
});
logoutHandler.addListener(() => {
updateEmailForm.reset();
clearFormValidity(updateEmailForm);
});
const resendEmailVerificationForm = $("#resend-email-verification-form");
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "resend-verify-email", token: csrfToken},
resendEmailVerificationForm,
() => {
refreshUserData();
showMessageSuccess(
resendEmailVerificationForm,
"An email with verification instructions has been sent and should arrive within a few minutes."
);
}
);
});
logoutHandler.addListener(() => clearFormValidity(resendEmailVerificationForm));
const toggleNotificationsForm = $("#toggle-notifications-form");
const notificationsEnabledCheckbox = $("#notifications-enabled-checkbox");
notificationsEnabledCheckbox.addEventListener("change", (event: Event) => {
event.preventDefault();
const enableNotifications = notificationsEnabledCheckbox.checked;
postApi(
{
action: "toggle-notifications",
enable_notifications: enableNotifications,
token: csrfToken
},
toggleNotificationsForm,
() => {
refreshUserData();
if (enableNotifications)
showInputValid($("#notifications-enabled-checkbox"), "Notifications have been enabled.");
else
showInputValid(
$("#notifications-enabled-checkbox"),
"Notifications have been disabled. " +
"You will still receive security notifications, for example if you change your email address " +
"or password."
);
}
);
});
logoutHandler.addListener(() => clearFormValidity(toggleNotificationsForm));
const updatePasswordForm = $("#update-password-form");
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-password",
token: csrfToken,
password_old: $("#update-password-password-old").value,
password_new: $("#update-password-password-new").value,
},
event.target as HTMLFormElement,
() => {
updatePasswordForm.reset();
refreshUserData();
showMessageSuccess(updatePasswordForm, "Password updated successfully!");
}
);
});
logoutHandler.addListener(() => {
updatePasswordForm.reset();
clearFormValidity(updatePasswordForm);
});
const forgotPasswordAfterLoginForm = $("#forgot-password-after-login-form");
$("#forgot-password-after-login-button").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
if (!window.confirm("Send an email to help reset your password?")) return;
postApi(
{
action: "send-password-reset",
token: csrfToken,
email: $("#forgot-password-after-login-email").value,
},
forgotPasswordAfterLoginForm,
() => {
showMessageSuccess(
forgotPasswordAfterLoginForm,
"An email with password reset instructions has been sent and should arrive within a few minutes."
);
}
);
});
logoutHandler.addListener(() => {
forgotPasswordAfterLoginForm.reset();
clearFormValidity(forgotPasswordAfterLoginForm);
});
const deleteForm = $("#delete-account-form");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
clearFormValidity(deleteForm);
const emailInput = $("#delete-account-email");
if (emailInput.value.trim() === "") {
showInputInvalid(emailInput, "Enter your current email address.");
return;
} else if (emailInput.value !== $("#delete-account-actual-email").value) {
showInputInvalid(emailInput, "Incorrect email address.");
return;
}
const confirmInput = $("#delete-account-confirm");
if (!confirmInput.checked) {
showInputInvalid(confirmInput, "Confirm that you want to delete your account.");
return;
}
postApi(
{action: "user-delete", token: csrfToken},
deleteForm,
() => {
logoutHandler.invokeListeners();
showMessageSuccess(
sharedMessageElement,
"Your account has been deleted. " +
"If you ever want to use Death Notifier again, you can always create a new account."
);
}
);
});
logoutHandler.addListener(() => {
deleteForm.reset();
clearFormValidity(deleteForm);
});
});
// Run initialization code

View File

@ -96,7 +96,7 @@ class AddTrackingAction extends Action
try {
$info = $this->wikipedia->query_person_info([$person_name]);
$normalized_name = $info->redirects[$person_name];
$normalized_name = $info->redirects->resolve($person_name);
} catch (WikipediaException $exception) {
throw new UnexpectedException(
"Could not reach Wikipedia. Maybe the website is down?",

View File

@ -4,6 +4,7 @@ namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\wikipedia\PersonStatus;
use com\fwdekker\deathnotifier\wikipedia\Redirects;
use PDO;
@ -209,10 +210,10 @@ class TrackingList
/**
* Renames people in the database.
*
* @param array<string, string> $renamings a map of all changes, from old name to new name
* @param Redirects $renamings a map of all changes, from old name to new name
* @return void
*/
public function rename_persons(array $renamings): void
public function rename_persons(Redirects $renamings): void
{
$this->transaction(function () use ($renamings) {
$conn = $this->database->conn;

View File

@ -57,7 +57,7 @@ class LoginAction extends Action
$user_data = $this->user_list->get_user_by_email($inputs["email"]);
if ($user_data === null)
throw new InvalidValueException("No user with that email address has been registered.", "password");
throw new InvalidValueException("No user with that email address has been registered.", "email");
if (!password_verify($inputs["password"], $user_data["password"]))
throw new InvalidValueException("Incorrect password.", "password");

View File

@ -15,9 +15,9 @@ class QueryOutput
*/
public readonly array $results;
/**
* @var array<string, string> mapping of queried names to normalized/redirected names
* @var Redirects mapping of queried names to normalized/redirected names
*/
public readonly array $redirects;
public readonly Redirects $redirects;
/**
* @var string[] list of missing articles
*/
@ -30,10 +30,10 @@ class QueryOutput
*
* @param array<string, T> $results the results of the query, either raw from {@see Wikipedia} or processed in some
* way
* @param array<string, string> $redirects mapping of queried names to normalized/redirected names
* @param Redirects $redirects mapping of queried names to normalized/redirected names
* @param string[] $missing list of missing articles
*/
public function __construct(array $results, array $redirects, array $missing)
public function __construct(array $results, Redirects $redirects, array $missing)
{
$this->results = $results;
$this->redirects = $redirects;

View File

@ -0,0 +1,92 @@
<?php
namespace com\fwdekker\deathnotifier\wikipedia;
use ArrayIterator;
use IteratorAggregate;
use Traversable;
/**
* Collects redirects and normalizations of Wikipedia article titles.
*
* Can contain reflexive redirects, i.e. a "redirect" from a title to the exact same title.
*
* @implements IteratorAggregate<string, string>
*/
class Redirects implements IteratorAggregate
{
/**
* @var array<string, string> the known redirects
*/
private readonly array $redirects;
/**
* Constructs a new `Redirects`.
*
* @param array<string, string> $redirects the redirects to start with
*/
public function __construct(array $redirects = [])
{
$this->redirects = $redirects;
}
/**
* Adds a single redirect, and returns a new `Redirects` containing the current redirects and the new redirects.
*
* If there already exists a redirect `a -> b`, and the redirect `b -> c` is added, then `a -> b` is changed to
* `a -> c`.
*
* If there already exists a redirect `b -> c`, and the redirect `a -> b` is added, then `a -> b` is added as
* `a -> c`.
*
* @param array<string, string> $redirects the redirects to add
* @return Redirects a new `Redirects` containing the current redirects and the new redirects
*/
public function add_redirects(array $redirects): Redirects
{
$new_redirects = $this->redirects;
foreach ($redirects as $from => $to) {
$already_redirected = array_search($from, $new_redirects);
$new_redirects[$already_redirected === false ? $from : $already_redirected] = $to;
}
return new Redirects($new_redirects);
}
/**
* Returns the page that {@see from} is redirect to, or {@see from} if there is no redirect.
*
* @param string $from the page to resolve the redirect of
* @return string the page that {@see from} is redirect to, or {@see from} if there is no redirect
*/
public function resolve(string $from): string
{
return !isset($this->redirects[$from]) ? $from : $this->redirects[$from];
}
/**
* Returns `true` if and only if there is at least one redirect that is not reflexive.
*
* @return bool `true` if and only if there is at least one redirect that is not reflexive
*/
public function has_non_reflexive_redirects(): bool
{
return array_keys($this->redirects) !== array_values($this->redirects);
}
/**
* Returns a {@see Traversable} to iterate over the redirects.
*
* @return Traversable<string, string> a {@see Traversable} to iterate over the redirects
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->redirects);
}
}

View File

@ -2,6 +2,7 @@
namespace com\fwdekker\deathnotifier\wikipedia;
use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\Util;
use JsonException;
@ -112,7 +113,7 @@ class Wikipedia
private function api_query_batched(array $params, array $titles, ?string $continue_name = null): QueryOutput
{
$articles = [];
$redirects = array_combine($titles, $titles);
$redirects = new Redirects();
$missing = [];
$title_chunks = array_chunk($titles, self::ARTICLES_PER_QUERY);
@ -127,15 +128,19 @@ class Wikipedia
$articles[strval($article["title"])] = $article;
}
$response_normalized = array_column($response["normalized"] ?? [], "to", "from");
foreach ($response_normalized as $from => $to)
$redirects[strval($from)] = strval($to);
$new_normalizations =
array_combine(
array_map(fn($it) => strval($it), array_column($response["normalized"] ?? [], "from")),
array_map(fn($it) => strval($it), array_column($response["normalized"] ?? [], "to"))
);
$redirects = $redirects->add_redirects($new_normalizations);
$response_redirects = array_column($response["redirects"] ?? [], "to", "from");
foreach ($response_redirects as $from => $to) {
$pre_normalized = array_search($from, $response_normalized);
$redirects[strval($pre_normalized === false ? $from : $pre_normalized)] = strval($to);
}
$new_redirects =
array_combine(
array_map(fn($it) => strval($it), array_column($response["redirects"] ?? [], "from")),
array_map(fn($it) => strval($it), array_column($response["redirects"] ?? [], "to"))
);
$redirects = $redirects->add_redirects($new_redirects);
}
return new QueryOutput($articles, $redirects, $missing);
@ -172,12 +177,12 @@ class Wikipedia
"prop" => "info",
"titles" => $title_after_move,
"redirects" => true,
]);
])["query"];
if (!empty($after_move_page_info->missing))
return $this->api_query_title_after_move($title, $max_depth - 1);
else if (!empty($after_move_page_info->redirects))
return array_values($after_move_page_info->redirects)[0];
if (in_array(-1, array_keys($after_move_page_info["pages"])))
return $this->api_query_title_after_move($title_after_move, $max_depth - 1);
else if (!empty($after_move_page_info["redirects"]))
return $after_move_page_info["redirects"][0]["to"];
else
return $title_after_move;
}
@ -207,15 +212,15 @@ class Wikipedia
$moves[$missing_title] = $title_after_move;
}
$output_of_moves = $this->api_query_batched($params, $moves, $continue_name);
if (array_keys($output_of_moves->redirects) !== array_values($output_of_moves->redirects))
$output_of_moves = $this->api_query_batched($params, array_values($moves), $continue_name);
if ($output_of_moves->redirects->has_non_reflexive_redirects())
throw new WikipediaException("Article was moved unexpectedly: " . json_encode($output_of_moves->redirects));
if (!empty($output_of_moves->missing))
throw new WikipediaException("Article missing unexpectedly: " . json_encode($output_of_moves->missing));
return new QueryOutput(
array_replace($output_base->results, $output_of_moves->results),
array_replace($output_base->redirects, $moves),
$output_base->redirects->add_redirects($moves),
$not_moved
);
}