Update code

This commit is contained in:
holo-gfx 2021-03-19 13:06:32 -07:00
parent cb9029387f
commit 3a8c3dc66b
126 changed files with 12738 additions and 3993 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
API_URL=https://api.mangadex.org/v2/

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
config.req.php
/images/manga
/images/avatars
/images/lists
/images/groups
/images/covers
/images/banners
/data
/data-drama
/data-saver
/delete
/dl/*
/transferred
/vendor
/node_modules
sitemap.xml
/db/
/dist/*
phpunit.xml
/.idea
.env*.local
composer.phar

View File

@ -1,3 +1,45 @@
Code vulenerable to a PHP RCE. **Edit a file, create a new file, and clone from Bitbucket in under 2 minutes**
IP Address for Site: 152.228.156.26 When you're done, you can delete the content in this README and update the file with details for others getting started with your repository.
*We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.*
---
## Edit a file
Youll start by editing this README file to learn how to edit a file in Bitbucket.
1. Click **Source** on the left side.
2. Click the README.md link from the list of files.
3. Click the **Edit** button.
4. Delete the following text: *Delete this line to make a change to the README from Bitbucket.*
5. After making your change, click **Commit** and then **Commit** again in the dialog. The commit page will open and youll see the change you just made.
6. Go back to the **Source** page.
---
## Create a file
Next, youll add a new file to this repository.
1. Click the **New file** button at the top of the **Source** page.
2. Give the file a filename of **contributors.txt**.
3. Enter your name in the empty file space.
4. Click **Commit** and then **Commit** again in the dialog.
5. Go back to the **Source** page.
Before you move on, go ahead and explore the repository. You've already seen the **Source** page, but check out the **Commits**, **Branches**, and **Settings** pages.
---
## Clone a repository
Use these steps to clone from SourceTree, our client for using the repository command-line free. Cloning allows you to work on your files locally. If you don't yet have SourceTree, [download and install first](https://www.sourcetreeapp.com/). If you prefer to clone from the command line, see [Clone a repository](https://confluence.atlassian.com/x/4whODQ).
1. Youll see the clone button under the **Source** heading. Click that button.
2. Now click **Check out in SourceTree**. You may need to create a SourceTree account or log in.
3. When you see the **Clone New** dialog in SourceTree, update the destination path and name if youd like to and then click **Clone**.
4. Open the directory you just created to see your repositorys files.
Now that you're more familiar with your Bitbucket repository, go ahead and add a new file locally. You can [push your change back to Bitbucket with SourceTree](https://confluence.atlassian.com/x/iqyBMg), or you can [add, commit,](https://confluence.atlassian.com/x/8QhODQ) and [push from the command line](https://confluence.atlassian.com/x/NQ0zDQ).

View File

@ -1,10 +1,5 @@
<?php <?php
//if (isset($_GET['_'])) {
// http_response_code(666);
// die();
//}
require_once ('../bootstrap.php'); require_once ('../bootstrap.php');
define('IS_NOJS', (isset($_GET['nojs']) && $_GET['nojs'])); define('IS_NOJS', (isset($_GET['nojs']) && $_GET['nojs']));
@ -174,13 +169,34 @@ switch ($function) {
$recipient_user = new User($thread->recipient_id, 'user_id'); $recipient_user = new User($thread->recipient_id, 'user_id');
$sender_user = new User($thread->sender_id, 'user_id'); $sender_user = new User($thread->sender_id, 'user_id');
// in the context of a pm thread, "sender" is the op rather than necessarily whoever is currently sending the message
// so in case the current replying user is the "recipient", flip the variables around to match the correct meaning
if ($thread->recipient_id == $user->user_id) {
$recipient_user = $sender_user;
$sender_user = $user;
}
/*$canReceiveDms = \validate_level($user, 'pr') // Staff can always send dms
|| ($recipient_user->dm_privacy ?? 0) < 1 // User has no dm restriction set
|| \in_array( // sender is a friend of recipient
$user->user_id,
\array_map(static function ($u) {
return $u['user_id'];
},
\array_filter($recipient_user->get_friends_user_ids(), static function ($u) {
return $u['accepted'] === 1;
})
),
true
);*/
$sender_blocked = $sender_user->get_blocked_user_ids(); $sender_blocked = $sender_user->get_blocked_user_ids();
$recipient_blocked = $recipient_user->get_blocked_user_ids(); $recipient_blocked = $recipient_user->get_blocked_user_ids();
// DM restriction if there is an active restriction and the sender isnt staff. restricted users can always message staff // DM restriction if there is an active restriction and the sender isnt staff. restricted users can always message staff
$dm_restriction = $user->has_active_restriction(USER_RESTRICTION_CREATE_DM) && !validate_level($recipient_user, 'mod'); $dm_restriction = $user->has_active_restriction(USER_RESTRICTION_CREATE_DM) && !validate_level($recipient_user, 'mod');
if (($user->user_id == $thread->sender_id || $user->user_id == $thread->recipient_id) && !isset($sender_blocked[$thread->recipient_id]) && !isset($recipient_blocked[$thread->sender_id]) && !$dm_restriction) { if (/*$canReceiveDms &&*/($user->user_id == $thread->sender_id || $user->user_id == $thread->recipient_id) && !isset($sender_blocked[$thread->recipient_id]) && !isset($recipient_blocked[$thread->sender_id]) && !$dm_restriction) {
$sql->modify('msg_reply', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text) VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$id, $user->user_id, $reply]); $sql->modify('msg_reply', ' INSERT INTO mangadex_pm_msgs (msg_id, thread_id, user_id, timestamp, text) VALUES (NULL, ?, ?, UNIX_TIMESTAMP(), ?) ', [$id, $user->user_id, $reply]);
if ($thread->sender_id == $user->user_id) if ($thread->sender_id == $user->user_id)
@ -199,6 +215,8 @@ switch ($function) {
$details = "You can't reply to the message because they are blocked."; $details = "You can't reply to the message because they are blocked.";
elseif (isset($recipient_blocked[$thread->sender_id])) elseif (isset($recipient_blocked[$thread->sender_id]))
$details = "You can't reply to the message because they are blocked."; $details = "You can't reply to the message because they are blocked.";
/*elseif (!$canReceiveDms)
$details = "You can't send messages to this user until you have accepted each other as friends.";*/
elseif ($dm_restriction) elseif ($dm_restriction)
$details = $user->get_restriction_message(USER_RESTRICTION_CREATE_DM) ?? "You can't reply to this dm."; $details = $user->get_restriction_message(USER_RESTRICTION_CREATE_DM) ?? "You can't reply to this dm.";
else else
@ -248,6 +266,20 @@ switch ($function) {
$recipient_id = $sql->prep('recipient_id', ' SELECT user_id FROM mangadex_users WHERE username = ?', [$recipient], 'fetchColumn', '', -1); $recipient_id = $sql->prep('recipient_id', ' SELECT user_id FROM mangadex_users WHERE username = ?', [$recipient], 'fetchColumn', '', -1);
$recipient_user = new User($recipient_id, 'user_id'); $recipient_user = new User($recipient_id, 'user_id');
$canReceiveDms = \validate_level($user, 'pr') // Staff can always send dms
|| ($recipient_user->dm_privacy ?? 0) < 1 // User has no dm restriction set
|| \in_array( // sender is a friend of recipient
$user->user_id,
\array_map(static function ($u) {
return $u['user_id'];
},
\array_filter($recipient_user->get_friends_user_ids(), static function ($u) {
return $u['accepted'] === 1;
})
),
true
);
$user_blocked = $user->get_blocked_user_ids(); $user_blocked = $user->get_blocked_user_ids();
$recipient_blocked = $recipient_user->get_blocked_user_ids(); $recipient_blocked = $recipient_user->get_blocked_user_ids();
@ -257,7 +289,7 @@ switch ($function) {
$has_banned_word = !validate_level($user, "pr") && (strpos_arr($message, SPAM_WORDS) !== FALSE || strpos_arr($subject, SPAM_WORDS) !== FALSE); $has_banned_word = !validate_level($user, "pr") && (strpos_arr($message, SPAM_WORDS) !== FALSE || strpos_arr($subject, SPAM_WORDS) !== FALSE);
$has_dmed_recently = !validate_level($user, "pr") && ($timestamp - $last_message_timestamp < 30); $has_dmed_recently = !validate_level($user, "pr") && ($timestamp - $last_message_timestamp < 30);
$is_valid_recipient = $recipient_id && $recipient_id != $user->user_id; $is_valid_recipient = $canReceiveDms && $recipient_id && $recipient_id != $user->user_id;
$is_blocked = isset($user_blocked[$recipient_id]) || isset($recipient_blocked[$user->user_id]); $is_blocked = isset($user_blocked[$recipient_id]) || isset($recipient_blocked[$user->user_id]);
if(!validate_level($user, 'member') || $dm_restriction){ if(!validate_level($user, 'member') || $dm_restriction){
@ -273,20 +305,17 @@ switch ($function) {
$memcached->delete("user_{$recipient_id}_unread_msgs"); $memcached->delete("user_{$recipient_id}_unread_msgs");
$details = $thread_id; $details = $thread_id;
} } else if ($has_dmed_recently) {
else if($has_dmed_recently) {
$details = "Please wait before sending another message."; $details = "Please wait before sending another message.";
} } else if (!$canReceiveDms) {
else if(!$is_valid_recipient) { $details = "You can't send messages to this user until you have accepted each other as friends.";
} else if (!$is_valid_recipient) {
$details = "$recipient is an invalid recipient."; $details = "$recipient is an invalid recipient.";
} } else if ($is_blocked) {
else if($is_blocked) {
$details = "$recipient has blocked you or you have blocked them."; $details = "$recipient has blocked you or you have blocked them.";
} } else if (!$captcha_validate['success']) {
else if(!$captcha_validate['success']) {
$details = 'You need to solve the captcha to send messages.'; $details = 'You need to solve the captcha to send messages.';
} } else {
else {
$thread_id = $sql->modify('msg_send', ' INSERT INTO mangadex_pm_threads (thread_id, thread_subject, sender_id, recipient_id, thread_timestamp, sender_read, recipient_read, sender_deleted, recipient_deleted) $thread_id = $sql->modify('msg_send', ' INSERT INTO mangadex_pm_threads (thread_id, thread_subject, sender_id, recipient_id, thread_timestamp, sender_read, recipient_read, sender_deleted, recipient_deleted)
VALUES (NULL, ?, ?, ?, UNIX_TIMESTAMP(), 1, 0, 0, 0) ', [$subject, $user->user_id, $recipient_id]); VALUES (NULL, ?, ?, ?, UNIX_TIMESTAMP(), 1, 0, 0, 0) ', [$subject, $user->user_id, $recipient_id]);
@ -545,6 +574,89 @@ switch ($function) {
$result = ($details) ? 0 : 1; $result = ($details) ? 0 : 1;
break; break;
case "banner_upload":
$file = $_FILES["file"];
$user_id = prepare_numeric($_POST["user_id"]);
$is_anonymous = isset($_POST["is_anonymous"]) ? 1 : 0;
$is_enabled = isset($_POST["is_enabled"]) ? 1 : 0;
$file_extension = strtolower(end(explode(".", $file["name"])));
if($file["error"] != UPLOAD_ERR_OK){
$error .= display_alert('danger', 'Failed', "File upload error.");
}
if(!validate_level($user, 'pr')) {
$error .= display_alert('danger', 'Failed', "You can't upload banners.");
}
if(!in_array($file_extension, ALLOWED_IMG_EXT)){
$error .= display_alert('danger', 'Failed', "Illegal file extension.");
}
if(!$error){
try {
$banner_id = $sql->modify('banner_upload',
"INSERT INTO mangadex_banners (user_id, is_anonymous, is_enabled, ext) VALUES (?, ?, ?, ?)",
[$user_id, $is_anonymous, $is_enabled, $file_extension]);
move_uploaded_file($file["tmp_name"], ABS_DATA_BASEPATH . "/banners/affiliatebanner$banner_id.$file_extension");
$memcached->delete("banners_all");
$memcached->delete("banners_enabled");
}
catch(Exception $e){
$error .= display_alert('danger', 'Failed', "Database error.");
}
}
if($error){
$details = $error;
print $error;
}
$result = $details ? 0 : 1;
break;
case "banner_edit":
$file = $_FILES["file"];
$banner_id = prepare_numeric($_GET["banner_id"]);
$user_id = prepare_numeric($_POST["user_id"]);
$is_anonymous = isset($_POST["is_anonymous"]) ? 1 : 0;
$is_enabled = isset($_POST["is_enabled"]) ? 1 : 0;
if($file["error"] == UPLOAD_ERR_NO_FILE){
$file_extension = $sql->prep("banner_ext", "SELECT ext FROM mangadex_banners WHERE banner_id = ?", [$banner_id], "fetch", PDO::FETCH_ASSOC, -1)["ext"];
}
else if($file["error"] == UPLOAD_ERR_OK){
$file_extension = strtolower(end(explode(".", $file["name"])));
}
else{
$error .= display_alert('danger', 'Failed', "File upload error.");
}
if(!validate_level($user, 'pr')) {
$error .= display_alert('danger', 'Failed', "You can't edit banners.");
}
if(!in_array($file_extension, ALLOWED_IMG_EXT)){
$error .= display_alert('danger', 'Failed', "Illegal file extension.");
}
if(!$error){
try {
$sql->modify('banner_edit',
"UPDATE mangadex_banners SET user_id = ?, is_anonymous = ?, is_enabled = ?, ext = ? WHERE banner_id = ?",
[$user_id, $is_anonymous, $is_enabled, $file_extension, $banner_id]);
if($file["error"] == UPLOAD_ERR_OK){
move_uploaded_file($file["tmp_name"], ABS_DATA_BASEPATH . "/banners/affiliatebanner$banner_id.$file_extension");
}
$memcached->delete("banners_all");
$memcached->delete("banners_enabled");
}
catch(Exception $e){
$error .= display_alert('danger', 'Failed', "Database error.");
}
}
if($error){
$details = $error;
print $error;
}
$result = $details ? 0 : 1;
break;
} }
/* /*
if (!in_array($function, ['manga_follow', 'manga_unfollow'])) if (!in_array($function, ['manga_follow', 'manga_unfollow']))

View File

@ -236,7 +236,7 @@ switch ($function) {
$to = $email1; $to = $email1;
$subject = "MangaDex: Account Creation - $username"; $subject = "MangaDex: Account Creation - $username";
$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) \n\nActivation code: $activation_key \n\nPlease visit " . URL . "activation/$activation_key to activate your account."; $body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) \n\nActivation code: $activation_key \n\nPlease visit " . URL . "activation/$activation_key to activate your account. \n\n If the above link doesn't work, try logging in and entering the activation code manually here " . URL . "activation instead.";
//$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) Due to problem with a spammer, activation codes are temporarily not being sent in this email. Please reply to this email to request an activation code. Apologies for the inconvenience!"; //$body = "Thank you for creating an account on MangaDex. \n\nUsername: $username \nPassword: (your chosen password) Due to problem with a spammer, activation codes are temporarily not being sent in this email. Please reply to this email to request an activation code. Apologies for the inconvenience!";
send_email($to, $subject, $body); send_email($to, $subject, $body);
@ -427,7 +427,7 @@ switch ($function) {
FROM mangadex_users u FROM mangadex_users u
JOIN mangadex_ip_bans b JOIN mangadex_ip_bans b
ON u.creation_ip = b.ip OR u.last_ip = b.ip ON u.creation_ip = b.ip OR u.last_ip = b.ip
WHERE user_id = ? LIMIT 1', [$user->user_id], "fetchAll", PDO::FETCH_UNIQUE, -1); WHERE user_id = ? LIMIT 1', [$user->user_id], "fetchColumn", '', -1);
if($user_banned){ if($user_banned){
$sql->modify('activate', ' UPDATE mangadex_users SET level_id = 0, activated = 1 WHERE user_id = ? AND activated = 0 LIMIT 1 ', [$user->user_id]); $sql->modify('activate', ' UPDATE mangadex_users SET level_id = 0, activated = 1 WHERE user_id = ? AND activated = 0 LIMIT 1 ', [$user->user_id]);
} }
@ -465,6 +465,39 @@ switch ($function) {
$result = 1; $result = 1;
break; break;
case "change_activation_email":
$email = $_POST['email'];
if($email != $user->email){
// check for another account with this email
$count_email = $sql->prep('count_email', ' SELECT count(*) FROM mangadex_users WHERE email = ? ', [$email], 'fetchColumn', '', -1);
//check for banned hosts
$banned_hosts = $sql->query_read('tempmail', "SELECT host FROM mangadex_tempmail ORDER BY host ASC ", 'fetchAll', PDO::FETCH_COLUMN);
$email_parts = explode('@', $email);
$banned_email = in_array($email_parts[1], $banned_hosts);
if($count_email || $banned_email){
$details = 'This email cannot be used.';
print display_alert('danger', 'Failed', $details); // wrong code
$result = 0;
}
else{
$sql->modify('change_email', ' UPDATE mangadex_users SET email = ? WHERE user_id = ? LIMIT 1 ', [$email, $user->user_id]);
$memcached->delete("user_$user->user_id");
$to = $email;
$subject = "MangaDex: Resend Activation Code - $user->username";
$body = "Here's your activation code. \n\nUsername: $user->username \n\nActivation code: $user->activation_key \n\nPlease visit " . URL . "activation/$user->activation_key to activate your account. ";
send_email($to, $subject, $body, 3);
$result = 1;
}
}
break;
case "2fa_setup": case "2fa_setup":
if ($user === null || $user->user_id < 2 || !validate_level($user, 'member')) { if ($user === null || $user->user_id < 2 || !validate_level($user, 'member')) {

View File

@ -738,6 +738,25 @@ switch ($function) {
$result = (!is_numeric($details)) ? 0 : 1; $result = (!is_numeric($details)) ? 0 : 1;
break; break;
case "manga_regenerate_thumb":
$id = prepare_numeric($_GET['id']);
if (validate_level($user, 'mod')) {
$manga = new Manga($id);
$ext = strtolower($manga->manga_image);
generate_thumbnail(ABS_DATA_BASEPATH . "/manga/$manga->manga_id.$ext", 1);
$details = $id;
}
else {
$details = "You can't regenerate this thumbnail.";
print display_alert('danger', 'Failed', $details); //fail
}
$result = (!is_numeric($details)) ? 0 : 1;
break;
case "manga_report": case "manga_report":
$id = prepare_numeric($_GET['id']); $id = prepare_numeric($_GET['id']);
$report_text = htmlentities($_POST["report_text"]); $report_text = htmlentities($_POST["report_text"]);

View File

@ -25,10 +25,10 @@ switch ($function) {
if (!$user->user_id) if (!$user->user_id)
$error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again."); $error .= display_alert('danger', 'Failed', "Your session has timed out. Please log in again.");
elseif ($upload < 80) elseif ($upload < 40)
$error .= display_alert('danger', 'Failed', "Your upload speed must be at least 80 Mbps."); $error .= display_alert('danger', 'Failed', "Your upload speed must be at least 40 Mbps.");
elseif ($download < 80) elseif ($download < 40)
$error .= display_alert('danger', 'Failed', "Your download speed must be at least 80 Mbps."); $error .= display_alert('danger', 'Failed', "Your download speed must be at least 40 Mbps.");
elseif ($upload > 65535) elseif ($upload > 65535)
$error .= display_alert('danger', 'Failed', "Your upload speed is too high."); $error .= display_alert('danger', 'Failed', "Your upload speed is too high.");
elseif ($download > 65535) elseif ($download > 65535)

View File

@ -298,6 +298,7 @@ switch ($function) {
$post_sensitivity = prepare_numeric($_POST['swipe_sensitivity']); $post_sensitivity = prepare_numeric($_POST['swipe_sensitivity']);
$reader_mode = prepare_numeric($_POST['reader_mode']) ?? 0; $reader_mode = prepare_numeric($_POST['reader_mode']) ?? 0;
$image_fit = prepare_numeric($_POST['image_fit']) ?? 0; $image_fit = prepare_numeric($_POST['image_fit']) ?? 0;
$data_saver = prepare_numeric($_POST['data_saver']) ?? 0;
$img_server = prepare_numeric($_POST['img_server']); $img_server = prepare_numeric($_POST['img_server']);
if ($reader_mode && $image_fit == 2) if ($reader_mode && $image_fit == 2)
$image_fit = 0; $image_fit = 0;
@ -312,6 +313,7 @@ switch ($function) {
$sql->modify('reader_settings', ' $sql->modify('reader_settings', '
UPDATE mangadex_users SET reader = ?, swipe_direction = ?, swipe_sensitivity = ?, reader_mode = ?, reader_click = ?, image_fit = ?, img_server = ? WHERE user_id = ? LIMIT 1 UPDATE mangadex_users SET reader = ?, swipe_direction = ?, swipe_sensitivity = ?, reader_mode = ?, reader_click = ?, image_fit = ?, img_server = ? WHERE user_id = ? LIMIT 1
', [$reader, $swipe_direction, $swipe_sensitivity, $reader_mode, $reader_click, $image_fit, $img_server, $user->user_id]); ', [$reader, $swipe_direction, $swipe_sensitivity, $reader_mode, $reader_click, $image_fit, $img_server, $user->user_id]);
$sql->modify('reader_settings', ' UPDATE mangadex_user_options SET data_saver = ? WHERE user_id = ? LIMIT 1 ', [(int) $data_saver, $user->user_id]);
$memcached->delete("user_$user->user_id"); $memcached->delete("user_$user->user_id");
} }
@ -328,6 +330,7 @@ switch ($function) {
$website = str_replace(['javascript:'], '', htmlentities($_POST['website'])); $website = str_replace(['javascript:'], '', htmlentities($_POST['website']));
$user_bio = str_replace(['javascript:'], '', htmlentities($_POST['user_bio'])); $user_bio = str_replace(['javascript:'], '', htmlentities($_POST['user_bio']));
$old_file = $_FILES['file']['name']; $old_file = $_FILES['file']['name'];
$email = $_POST['email'];
// Make sure website has http:// // Make sure website has http://
if (!empty($website) && stripos($website, 'http://') === false && stripos($website, 'https://') === false) if (!empty($website) && stripos($website, 'http://') === false && stripos($website, 'https://') === false)
@ -351,6 +354,21 @@ switch ($function) {
} }
} }
if($email != $user->email){
// check for another account with this email
$count_email = $sql->prep('count_email', ' SELECT count(*) FROM mangadex_users WHERE email = ? ', [$email], 'fetchColumn', '', -1);
//check for banned hosts
$banned_hosts = $sql->query_read('tempmail', "SELECT host FROM mangadex_tempmail ORDER BY host ASC ", 'fetchAll', PDO::FETCH_COLUMN);
$email_parts = explode('@', $email);
$banned_email = in_array($email_parts[1], $banned_hosts);
if($count_email || $banned_email){
$fail_reason = "This email cannot be used.";
$error .= display_alert("danger", "Failed", $fail_reason);
}
}
if (!$user->user_id) if (!$user->user_id)
$error .= display_alert('danger', 'Failed', 'Your session has timed out. Please log in again.'); //success $error .= display_alert('danger', 'Failed', 'Your session has timed out. Please log in again.'); //success
@ -358,7 +376,7 @@ switch ($function) {
$error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success $error .= display_alert('danger', 'Failed', 'You need to be at least a member.'); //success
if (!$error) { if (!$error) {
$sql->modify('change_profile', ' UPDATE mangadex_users SET language = ?, user_website = ?, user_bio = ? WHERE user_id = ? LIMIT 1 ', [$lang_id, $website, $user_bio, $user->user_id]); $sql->modify('change_profile', ' UPDATE mangadex_users SET language = ?, user_website = ?, user_bio = ?, email = ? WHERE user_id = ? LIMIT 1 ', [$lang_id, $website, $user_bio, $email, $user->user_id]);
if ($old_file) { if ($old_file) {
$arr = explode('.', $_FILES['file']['name']); $arr = explode('.', $_FILES['file']['name']);
@ -399,8 +417,9 @@ switch ($function) {
$theme_id = prepare_numeric($_POST['theme_id']); $theme_id = prepare_numeric($_POST['theme_id']);
$navigation = prepare_numeric($_POST['navigation']); $navigation = prepare_numeric($_POST['navigation']);
$list_privacy = prepare_numeric($_POST['list_privacy']); $list_privacy = prepare_numeric($_POST['list_privacy']);
$dm_privacy = prepare_numeric($_POST['dm_privacy']);
$reader = $_POST['reader'] ?? 0; $reader = $_POST['reader'] ?? 0;
$data_saver = $_POST['data_saver'] ?? 0; $port_limit = prepare_numeric($_POST['mdh_portlimit'] ?? 0);
$display_lang_id = prepare_numeric($_POST['display_lang_id']); $display_lang_id = prepare_numeric($_POST['display_lang_id']);
$old_file = $_FILES['file']['name']; $old_file = $_FILES['file']['name'];
$hentai_mode = prepare_numeric($_POST["hentai_mode"]); $hentai_mode = prepare_numeric($_POST["hentai_mode"]);
@ -423,10 +442,10 @@ switch ($function) {
if (!$error) { if (!$error) {
$sql->modify('site_settings', ' $sql->modify('site_settings', '
UPDATE mangadex_users SET hentai_mode = ?, display_moderated = ?, latest_updates = ?, reader = ?, default_lang_ids = ?, style = ?, display_lang_id = ?, list_privacy = ?, excluded_genres = ?, navigation = ?, show_unavailable = ? WHERE user_id = ? LIMIT 1 UPDATE mangadex_users SET hentai_mode = ?, display_moderated = ?, latest_updates = ?, reader = ?, default_lang_ids = ?, style = ?, display_lang_id = ?, list_privacy = ?, excluded_genres = ?, navigation = ?, dm_privacy = ?, show_unavailable = ? WHERE user_id = ? LIMIT 1
', [$hentai_mode, $display_moderated, $latest_updates, (int) $reader, $default_lang_ids, $theme_id, $display_lang_id, $list_privacy, implode(',', $excluded_genres), $navigation, $show_unavailable, $user->user_id]); ', [$hentai_mode, $display_moderated, $latest_updates, (int) $reader, $default_lang_ids, $theme_id, $display_lang_id, $list_privacy, implode(',', $excluded_genres), $navigation, $dm_privacy, $show_unavailable, $user->user_id]);
$sql->modify('site_settings', ' UPDATE mangadex_user_options SET data_saver = ? WHERE user_id = ? LIMIT 1 ', [(int) $data_saver, $user->user_id]); $sql->modify('site_settings', ' UPDATE mangadex_user_options SET mdh_portlimit = ? WHERE user_id = ? LIMIT 1 ', [$port_limit, $user->user_id]);
if ($old_file && !$reset_list_banner) { if ($old_file && !$reset_list_banner) {
$arr = explode(".", $_FILES["file"]["name"]); $arr = explode(".", $_FILES["file"]["name"]);

View File

@ -280,35 +280,17 @@ switch ($type) {
$page_array = array_combine(range(1, count($arr)), array_values($arr)); $page_array = array_combine(range(1, count($arr)), array_values($arr));
} }
$server_fallback = LOCAL_SERVER_URL; $server_fallback = IMG_SERVER_URL;
$server_network = null; $server_network = null;
// when a chapter does not exist on the local webserver, it gets an id. since all imageservers share the same data, we can assign any imageserver // use md@h for all images
// with the best location to the user. try {
if ($chapter->server > 0) { $subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP, $user->mdh_portlimit ?? false);
if (isset($user->md_at_home) && $user->md_at_home && stripos($chapter->page_order, 'http') === false) { if (!empty($subsubdomain)) {
try { $server_network = $subsubdomain;
$subsubdomain = $mdAtHomeClient->getServerUrl($chapter->chapter_hash, explode(',', $chapter->page_order), _IP);
if (!empty($subsubdomain)) {
$server_network = $subsubdomain;
}
} catch (Throwable $t) {
trigger_error($t->getMessage(), E_USER_WARNING);
}
}
$server_id = -1;
// If a usersetting overwrites it, take this
if (isset($_GET['server'])) {
// if the parameter was trash, this returns -1
$server_id = get_server_id_by_code($_GET['server']);
}
if ($server_id < 1) {
// Try to select a region based server if we havent set one already
$server_id = get_server_id_by_geography();
}
if ($server_id > 0) {
$server_fallback = "https://s$server_id.mangadex.org";
} }
} catch (\Throwable $t) {
trigger_error($t->getMessage(), E_USER_WARNING);
} }
$server = $server_network ?: $server_fallback; $server = $server_network ?: $server_fallback;
@ -341,11 +323,16 @@ switch ($type) {
if (!empty($server_network)) { if (!empty($server_network)) {
$array['server_fallback'] = $server_fallback.$data_dir; $array['server_fallback'] = $server_fallback.$data_dir;
} }
$isRestricted = in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA;
$countryCode = strtoupper(get_country_code($user->last_ip));
$isRegionBlocked = isset(REGION_BLOCKED_MANGA[$countryCode]) && in_array($manga->manga_id, REGION_BLOCKED_MANGA[$countryCode]) && !validate_level($user, 'pr');
if ($status === 'external') { if ($status === 'external') {
$array['external'] = $chapter->page_order; $array['external'] = $chapter->page_order;
} }
elseif (in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) { elseif ($isRestricted || $isRegionBlocked) {
$array = [ $array = [
'id' => $chapter->chapter_id, 'id' => $chapter->chapter_id,
'status' => 'restricted', 'status' => 'restricted',

View File

@ -11,6 +11,7 @@ use Mangadex\Controller\API\MangaController;
use Mangadex\Controller\API\RelationTypeController; use Mangadex\Controller\API\RelationTypeController;
use Mangadex\Controller\API\TagController; use Mangadex\Controller\API\TagController;
use Mangadex\Controller\API\UserController; use Mangadex\Controller\API\UserController;
use Mangadex\Controller\API\HighestChapterIDController;
use Mangadex\Exception\Http\HttpException; use Mangadex\Exception\Http\HttpException;
use Mangadex\Exception\Http\NotFoundHttpException; use Mangadex\Exception\Http\NotFoundHttpException;
use Mangadex\Exception\Http\TooManyRequestsHttpException; use Mangadex\Exception\Http\TooManyRequestsHttpException;
@ -67,6 +68,9 @@ try {
case 'index': case 'index':
$controller = new IndexController(); $controller = new IndexController();
break; break;
case 'highest_chapter_id':
$controller = new HighestChapterIDController();
break;
default: default:
throw new NotFoundHttpException("Invalid endpoint"); throw new NotFoundHttpException("Invalid endpoint");
break; break;

View File

@ -8,6 +8,7 @@ define('DB_USER', 'mangadex');
define('DB_PASSWORD', ''); define('DB_PASSWORD', '');
define('DB_NAME', 'mangadex'); define('DB_NAME', 'mangadex');
define('DB_HOST', 'localhost'); define('DB_HOST', 'localhost');
define('DB_PERSISTENT', false);
define('DB_READ_HOSTS', ['127.0.0.1']); define('DB_READ_HOSTS', ['127.0.0.1']);
define('DB_READ_NAME', DB_NAME); define('DB_READ_NAME', DB_NAME);
@ -29,6 +30,8 @@ define('URL', 'https://mangadex.org/');
define('TITLE', 'MangaDex'); define('TITLE', 'MangaDex');
define('DESCRIPTION', 'Read manga online for free at MangaDex with no ads, high quality images and support scanlation groups!'); define('DESCRIPTION', 'Read manga online for free at MangaDex with no ads, high quality images and support scanlation groups!');
define('MEMCACHED_HOST', '127.0.0.1'); define('MEMCACHED_HOST', '127.0.0.1');
define('MEMCACHED_SYNC_HOST', null);
define('MEMCACHED_SYNC_PORT', null);
define('GOOGLE_CAPTCHA_SITEKEY', 'xxx'); define('GOOGLE_CAPTCHA_SITEKEY', 'xxx');
define('GOOGLE_CAPTCHA_SECRET', 'xxx'); define('GOOGLE_CAPTCHA_SECRET', 'xxx');
@ -60,6 +63,7 @@ define('GOOGLE_SERVICE_ACCOUNT_PATH', '/var/www/google_service_credentials.json'
define('MAX_CHAPTER_FILESIZE', 104857600); //100*1024*1024 define('MAX_CHAPTER_FILESIZE', 104857600); //100*1024*1024
define('DMS_DISPLAY_LIMIT', 25); define('DMS_DISPLAY_LIMIT', 25);
define('PRIVATE_API_KEY', sha1('secretpass_changeme'));
define('REQUIRE_LOGIN_PAGES', ['users', 'follows', 'followed_manga', 'followed_groups', 'follows_import', 'upload', 'settings', 'messages', 'message', 'send_message', 'activation', 'admin', 'mod', 'group_new', 'manga_new', 'stats', 'social']); define('REQUIRE_LOGIN_PAGES', ['users', 'follows', 'followed_manga', 'followed_groups', 'follows_import', 'upload', 'settings', 'messages', 'message', 'send_message', 'activation', 'admin', 'mod', 'group_new', 'manga_new', 'stats', 'social']);
@ -90,8 +94,9 @@ define('MAX_IMAGE_FILESIZE', 1048576);
define('ALLOWED_IMG_EXT', ['jpg', 'jpeg', 'png', 'gif']); define('ALLOWED_IMG_EXT', ['jpg', 'jpeg', 'png', 'gif']);
define('ALLOWED_MIME_TYPES', ['image/png', 'image/jpeg', 'image/gif']); define('ALLOWED_MIME_TYPES', ['image/png', 'image/jpeg', 'image/gif']);
define('IMAGE_SERVER', 0); define('IMAGE_SERVER', 0);
define('IMG_SERVER_URL', 'https://s1.mangadex.org'); define('IMG_SERVER_URL', 'https://s2.mangadex.org');
define('LOCAL_SERVER_URL', 'https://cdndex.com/data/'); define('LOCAL_SERVER_URL', 'https://cdndex.com/data/');
define('API_V2_URL', 'https://api.mangadex.org/v2/');
//$server_array = ['eu2' => 1, 'na' => 2, 'eu' => 3, 'na2' => 4, 'na3' => 5]; //$server_array = ['eu2' => 1, 'na' => 2, 'eu' => 3, 'na2' => 4, 'na3' => 5];
define('IMAGE_SERVER_INFO', [ define('IMAGE_SERVER_INFO', [

View File

@ -9,6 +9,9 @@ require_once (__DIR__.'/../bootstrap.php');
require_once (ABSPATH . "/scripts/header.req.php"); require_once (ABSPATH . "/scripts/header.req.php");
echo "START @ ".date("F j, Y, g:i a")."\n";
echo "prune remote files ...\n";
// prune remote file upload tmpfiles // prune remote file upload tmpfiles
$dirh = opendir(sys_get_temp_dir()); $dirh = opendir(sys_get_temp_dir());
$nameFormat = 'remote_file_dl_'; $nameFormat = 'remote_file_dl_';
@ -23,13 +26,14 @@ while (false !== ($entry = readdir($dirh))) {
} }
//updated featured //updated featured
echo "featured ...\n";
$memcached->delete('featured'); $memcached->delete('featured');
$manga_lists = new Manga_Lists(); $manga_lists = new Manga_Lists();
$array_of_featured_manga_ids = $manga_lists->get_manga_list(11); $array_of_featured_manga_ids = $manga_lists->get_manga_list(12);
if (!empty($array_of_featured_manga_ids)) { if (!empty($array_of_featured_manga_ids)) {
$manga_ids_in = prepare_in($array_of_featured_manga_ids); $manga_ids_in = prepare_in($array_of_featured_manga_ids);
$featured = $sql->prep('featured', " $featured = $sql->prep('featured', "
SELECT chapters.manga_id, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp, SELECT /*+ MAX_EXECUTION_TIME(1800000) */ chapters.manga_id, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp,
mangas.manga_name, mangas.manga_image, mangas.manga_hentai, mangas.manga_bayesian, mangas.manga_name, mangas.manga_image, mangas.manga_hentai, mangas.manga_bayesian,
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows (SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
FROM mangadex_chapters AS chapters FROM mangadex_chapters AS chapters
@ -40,51 +44,55 @@ if (!empty($array_of_featured_manga_ids)) {
AND mangas.manga_id IN ($manga_ids_in) AND mangas.manga_id IN ($manga_ids_in)
GROUP BY chapters.manga_id GROUP BY chapters.manga_id
ORDER BY chapters.chapter_views DESC ORDER BY chapters.chapter_views DESC
", $array_of_featured_manga_ids , 'fetchAll', PDO::FETCH_ASSOC, 3600); ", $array_of_featured_manga_ids , 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
} }
//update new manga //update new manga
echo "new_manga ...\n";
$memcached->delete('new_manga'); $memcached->delete('new_manga');
$new_manga = $sql->query_read('new_manga', " $new_manga = $sql->prep('new_manga', "
SELECT mangas.manga_id, mangas.manga_name, mangas.manga_image, mangas.manga_hentai, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_name, mangas.manga_image, mangas.manga_hentai, chapters.chapter_id, chapters.chapter_views, chapters.chapter, chapters.upload_timestamp
FROM mangadex_mangas AS mangas FROM mangadex_mangas AS mangas
LEFT JOIN mangadex_chapters AS chapters LEFT JOIN mangadex_chapters AS chapters
ON mangas.manga_id = chapters.manga_id ON mangas.manga_id = chapters.manga_id
WHERE mangas.manga_hentai = 0 AND chapters.chapter_id IS NOT NULL WHERE mangas.manga_hentai = 0 AND chapters.chapter_id IS NOT NULL
GROUP BY mangas.manga_id GROUP BY mangas.manga_id
ORDER BY mangas.manga_id DESC LIMIT 10 ORDER BY mangas.manga_id DESC LIMIT 10
", 'fetchAll', PDO::FETCH_ASSOC, 3600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
//update top follows //update top follows
echo "top_follows ...\n";
$memcached->delete('top_follows'); $memcached->delete('top_follows');
$top_follows = $sql->query_read('top_follows', " $top_follows = $sql->prep('top_follows', "
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian, SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop, (SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows (SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
FROM mangadex_mangas AS mangas FROM mangadex_mangas AS mangas
WHERE mangas.manga_hentai = 0 WHERE mangas.manga_hentai = 0
ORDER BY count_follows DESC LIMIT 10 ORDER BY count_follows DESC LIMIT 10
", 'fetchAll', PDO::FETCH_ASSOC, 3600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
//update top rating //update top rating
echo "top_rating ...\n";
$memcached->delete('top_rating'); $memcached->delete('top_rating');
$top_rating = $sql->query_read('top_rating', " $top_rating = $sql->prep('top_rating', "
SELECT mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian, SELECT /*+ MAX_EXECUTION_TIME(1800000) */ mangas.manga_id, mangas.manga_image, mangas.manga_name, mangas.manga_hentai, mangas.manga_bayesian,
(SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop, (SELECT count(*) FROM mangadex_manga_ratings WHERE mangadex_manga_ratings.manga_id = mangas.manga_id) AS count_pop,
(SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows (SELECT count(*) FROM mangadex_follow_user_manga WHERE mangadex_follow_user_manga.manga_id = mangas.manga_id) AS count_follows
FROM mangadex_mangas AS mangas FROM mangadex_mangas AS mangas
WHERE mangas.manga_hentai = 0 WHERE mangas.manga_hentai = 0
ORDER BY manga_bayesian DESC LIMIT 10 ORDER BY manga_bayesian DESC LIMIT 10
", 'fetchAll', PDO::FETCH_ASSOC, 3600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 3600, true);
//process logs //process logs
$last_timestamp = $sql->query_read('last_timestamp', " SELECT visit_timestamp FROM mangadex_logs_visits ORDER BY visit_timestamp ASC LIMIT 1 ", 'fetchColumn', '', -1) + 3600; echo "last_timestamp ...\n";
$last_timestamp = $sql->prep('last_timestamp', " SELECT /*+ MAX_EXECUTION_TIME(1800000) */ visit_timestamp FROM mangadex_logs_visits ORDER BY visit_timestamp ASC LIMIT 1 ", [], 'fetchColumn', '', -1, true) + 3600;
for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) { for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) {
$views_guests = $sql->query_read('views_guests', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 ", 'fetchColumn', '', -1); $views_guests = $sql->prep('views_guests', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 ", [], 'fetchColumn', '', -1, true);
$views_logged_in = $sql->query_read('views_logged_in', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 ", 'fetchColumn', '', -1); $views_logged_in = $sql->prep('views_logged_in', " SELECT count(*) FROM mangadex_logs_visits WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 ", [], 'fetchColumn', '', -1, true);
$users_guests = $sql->query_read('users_guests', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 GROUP BY `visit_ip`) AS `TABLE` ", 'fetchColumn', '', -1); $users_guests = $sql->prep('users_guests', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id = 0 GROUP BY `visit_ip`) AS `TABLE` ", [], 'fetchColumn', '', -1, true);
$users_logged_in = $sql->query_read('users_logged_in', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 GROUP BY `visit_user_id`) AS `TABLE` ", 'fetchColumn', '', -1); $users_logged_in = $sql->prep('users_logged_in', " SELECT COUNT(*) FROM (SELECT `visit_user_id` FROM `mangadex_logs_visits` WHERE visit_timestamp >= ($i - 3600) AND visit_timestamp < $i AND visit_user_id > 0 GROUP BY `visit_user_id`) AS `TABLE` ", [], 'fetchColumn', '', -1, true);
$sql->modify('insert', ' INSERT INTO `mangadex_logs_visits_summary` (`id`, `timestamp`, `users_guests`, `users_logged_in`, `views_guests`, `views_logged_in`) VALUES (NULL, ?, ?, ?, ?, ?) ', [$i, $users_guests, $users_logged_in, $views_guests, $views_logged_in]); $sql->modify('insert', ' INSERT INTO `mangadex_logs_visits_summary` (`id`, `timestamp`, `users_guests`, `users_logged_in`, `views_guests`, `views_logged_in`) VALUES (NULL, ?, ?, ?, ?, ?) ', [$i, $users_guests, $users_logged_in, $views_guests, $views_logged_in]);
$sql->modify('delete', ' DELETE FROM `mangadex_logs_visits` WHERE visit_timestamp >= (? - 3600) AND visit_timestamp < ? ', [$i, $i]); $sql->modify('delete', ' DELETE FROM `mangadex_logs_visits` WHERE visit_timestamp >= (? - 3600) AND visit_timestamp < ? ', [$i, $i]);
@ -92,16 +100,22 @@ for($i = $last_timestamp; $i < ($last_timestamp + 3600); $i+=3600) {
} }
// Prune old chapter_history data // Prune old chapter_history data
echo "prune_manga_history ...\n";
$cutoff = time() - (60 * 60 * 24 * 90); // 90 days $cutoff = time() - (60 * 60 * 24 * 90); // 90 days
$sql->modify('prune_manga_history', 'DELETE FROM mangadex_manga_history WHERE `timestamp` < ?', [$cutoff]); $sql->modify('prune_manga_history', 'DELETE FROM mangadex_manga_history WHERE `timestamp` < ?', [$cutoff]);
// Prune expired ip bans // Prune expired ip bans
echo "prune_ip_bans ...\n";
$sql->modify('prune_ip_bans', 'DELETE FROM mangadex_ip_bans WHERE expires < UNIX_TIMESTAMP()', []); $sql->modify('prune_ip_bans', 'DELETE FROM mangadex_ip_bans WHERE expires < UNIX_TIMESTAMP()', []);
// Prune expired sessions each month on the 1st // Prune expired sessions each month on the 1st
if (date('j') == 1 && date('G') < 1) { if (date('j') == 1 && date('G') < 1) {
echo "prune_sessions ...\n";
$sql->modify('prune_sessions', 'DELETE FROM mangadex_sessions WHERE (created + ?) < UNIX_TIMESTAMP()', [60*60*24*365]); $sql->modify('prune_sessions', 'DELETE FROM mangadex_sessions WHERE (created + ?) < UNIX_TIMESTAMP()', [60*60*24*365]);
} }
//prune old chapter_live_views for trending data //prune old chapter_live_views for trending data
echo "prune_trending ...\n";
$sql->modify('prune_trending', 'DELETE FROM `mangadex_chapter_live_views` WHERE (timestamp + ?) < UNIX_TIMESTAMP()', [60*60*25]); $sql->modify('prune_trending', 'DELETE FROM `mangadex_chapter_live_views` WHERE (timestamp + ?) < UNIX_TIMESTAMP()', [60*60*25]);
echo "END @ ".date("F j, Y, g:i a")."\n";

View File

@ -1,15 +1,19 @@
<?php <?php
if (PHP_SAPI !== 'cli') if (PHP_SAPI !== 'cli') {
die(); die();
}
echo "START @ ".date("F j, Y, g:i a")."\n";
require_once (__DIR__.'/../bootstrap.php'); require_once (__DIR__.'/../bootstrap.php');
require_once (ABSPATH . "/scripts/header.req.php"); require_once (ABSPATH . "/scripts/header.req.php");
echo "latest_manga_comments ...\n";
$memcached->delete("latest_manga_comments"); $memcached->delete("latest_manga_comments");
$latest_manga_comments = $sql->query_read('latest_manga_comments', " $latest_manga_comments = $sql->prep('latest_manga_comments', "
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, mangas.manga_name, mangas.manga_id, SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, mangas.manga_name, mangas.manga_id,
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts (SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
WHERE mangadex_forum_posts.post_id <= posts.post_id WHERE mangadex_forum_posts.post_id <= posts.post_id
AND mangadex_forum_posts.thread_id = posts.thread_id AND mangadex_forum_posts.thread_id = posts.thread_id
@ -21,11 +25,12 @@ $latest_manga_comments = $sql->query_read('latest_manga_comments', "
ON threads.thread_name = mangas.manga_id ON threads.thread_name = mangas.manga_id
WHERE threads.forum_id = 11 AND threads.thread_deleted = 0 WHERE threads.forum_id = 11 AND threads.thread_deleted = 0
ORDER BY timestamp DESC LIMIT 10 ORDER BY timestamp DESC LIMIT 10
", 'fetchAll', PDO::FETCH_ASSOC, 600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
echo "latest_forum_posts ...\n";
$memcached->delete("latest_forum_posts"); $memcached->delete("latest_forum_posts");
$latest_forum_posts = $sql->query_read('latest_forum_posts', " $latest_forum_posts = $sql->prep('latest_forum_posts', "
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name, SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts (SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
WHERE mangadex_forum_posts.post_id <= posts.post_id WHERE mangadex_forum_posts.post_id <= posts.post_id
AND mangadex_forum_posts.thread_id = posts.thread_id AND mangadex_forum_posts.thread_id = posts.thread_id
@ -37,11 +42,12 @@ $latest_forum_posts = $sql->query_read('latest_forum_posts', "
ON threads.forum_id = forums.forum_id ON threads.forum_id = forums.forum_id
WHERE threads.forum_id NOT IN (11, 12, 14, 17, 18, 20) AND threads.thread_deleted = 0 WHERE threads.forum_id NOT IN (11, 12, 14, 17, 18, 20) AND threads.thread_deleted = 0
ORDER BY timestamp DESC LIMIT 10 ORDER BY timestamp DESC LIMIT 10
", 'fetchAll', PDO::FETCH_ASSOC, 600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
echo "latest_news_posts ...\n";
$memcached->delete("latest_news_posts"); $memcached->delete("latest_news_posts");
$latest_forum_posts = $sql->query_read('latest_news_posts', " $latest_forum_posts = $sql->prep('latest_news_posts', "
SELECT posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name, SELECT /*+ MAX_EXECUTION_TIME(600000) */ posts.post_id, posts.text, posts.timestamp, posts.thread_id, threads.thread_name, forums.forum_name,
(SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts (SELECT (count(*) -1) DIV 20 + 1 FROM mangadex_forum_posts
WHERE mangadex_forum_posts.post_id <= posts.post_id WHERE mangadex_forum_posts.post_id <= posts.post_id
AND mangadex_forum_posts.thread_id = posts.thread_id AND mangadex_forum_posts.thread_id = posts.thread_id
@ -53,16 +59,17 @@ $latest_forum_posts = $sql->query_read('latest_news_posts', "
ON threads.forum_id = forums.forum_id ON threads.forum_id = forums.forum_id
WHERE threads.forum_id = 26 AND threads.thread_sticky = 1 WHERE threads.forum_id = 26 AND threads.thread_sticky = 1
ORDER BY timestamp ASC LIMIT 1 ORDER BY timestamp ASC LIMIT 1
", 'fetchAll', PDO::FETCH_ASSOC, 600); ", [], 'fetchAll', PDO::FETCH_ASSOC, 600, true);
/// ///
/// Put delayed chapters that just expired into the last_updated table /// Put delayed chapters that just expired into the last_updated table
/// ///
echo "expired_delayed_chapters ... \n";
// Collect all chapters that have been uploaded as delayed, but where the delay is expired // Collect all chapters that have been uploaded as delayed, but where the delay is expired
$expired_delayed_chapters = $sql->query_read('expired_delayed_chapters', ' $expired_delayed_chapters = $sql->prep('expired_delayed_chapters', '
SELECT c.chapter_id, c.manga_id, c.volume, c.chapter, c.title, c.upload_timestamp, c.user_id, c.lang_id, c.group_id, c.group_id_2, c.group_id_3, c.available FROM mangadex_chapters c, mangadex_delayed_chapters d WHERE d.upload_timestamp < UNIX_TIMESTAMP() AND c.chapter_id = d.chapter_id SELECT /*+ MAX_EXECUTION_TIME(600000) */ c.chapter_id, c.manga_id, c.volume, c.chapter, c.title, c.upload_timestamp, c.user_id, c.lang_id, c.group_id, c.group_id_2, c.group_id_3, c.available FROM mangadex_chapters c, mangadex_delayed_chapters d WHERE d.upload_timestamp < UNIX_TIMESTAMP() AND c.chapter_id = d.chapter_id
', 'fetchAll', PDO::FETCH_ASSOC, -1); ', [], 'fetchAll', PDO::FETCH_ASSOC, -1, true);
// Only process if we found any // Only process if we found any
if (!empty($expired_delayed_chapters)) { if (!empty($expired_delayed_chapters)) {
// Collect all chapter ids in this array, so we can unset them as delayed after this // Collect all chapter ids in this array, so we can unset them as delayed after this
@ -94,22 +101,31 @@ INSERT INTO mangadex_last_updated (`chapter_id`, `manga_id`, `volume`, `chapter`
$sql->modify('unexpire_delayed_chapters', 'DELETE FROM mangadex_delayed_chapters WHERE chapter_id IN ('.$unexpire_in.')', []); $sql->modify('unexpire_delayed_chapters', 'DELETE FROM mangadex_delayed_chapters WHERE chapter_id IN ('.$unexpire_in.')', []);
} }
$stats = $mdAtHomeClient->getStatus(); echo "mdAtHomeClient->getStatus(); ... ";
try {
$stats = $mdAtHomeClient->getStatus();
foreach ($stats as $client) { foreach ($stats as $client) {
$client_id = (int) $client['client_id']; $client_id = (int) $client['client_id'];
$user_id = (int) $client['user_id']; $user_id = (int) $client['user_id'];
$subsubdomain = substr($client['url'], 8, 27); $subsubdomain = substr($client['url'], 8, 27);
if (strpos($subsubdomain, '.') === 0) if (strpos($subsubdomain, '.') === 0)
$subsubdomain = '*.' . substr($client['url'], 9, 13); $subsubdomain = '*.' . substr($client['url'], 9, 13);
$url = explode(':', $client['url']); $url = explode(':', $client['url']);
$port = (int) $url[2]; $port = (int) $url[2];
$client_ip = $client['ip']; $client_ip = $client['ip'];
$available = (int) $client['available']; $available = (int) $client['available'];
$shard_count = (int) $client['shard_count']; $shard_count = (int) $client['shard_count'];
$speed = ((int) $client['speed']) / 125000; $speed = ((int) $client['speed']) / 125000;
$images_served = (int) $client['images_served']; $images_served = (int) $client['images_served'];
$images_failed = (int) $client['images_failed']; $images_failed = (int) $client['images_failed'];
$bytes_served = (int) $client['bytes_served']; $bytes_served = (int) $client['bytes_served'];
$sql->modify('x', " update mangadex_clients set upload_speed = ?, client_ip = ?, client_subsubdomain = ?, client_port = ?, client_available = ?, shard_count = ?, images_served = ?, images_failed = ?, bytes_served = ?, update_timestamp = UNIX_TIMESTAMP() WHERE client_id = ? AND user_id = ? LIMIT 1 ", [$speed, $client_ip, $subsubdomain, $port, $available, $shard_count, $images_served, $images_failed, $bytes_served, $client_id, $user_id]); $sql->modify('x', " update mangadex_clients set upload_speed = ?, client_ip = ?, client_subsubdomain = ?, client_port = ?, client_available = ?, shard_count = ?, images_served = ?, images_failed = ?, bytes_served = ?, update_timestamp = UNIX_TIMESTAMP() WHERE client_id = ? AND user_id = ? LIMIT 1 ", [$speed, $client_ip, $subsubdomain, $port, $available, $shard_count, $images_served, $images_failed, $bytes_served, $client_id, $user_id]);
}
echo "OK\n";
} catch (\Throwable $t) {
echo "FAIL: ".$t->getMessage()."\n";
} }
echo "END @ ".date("F j, Y, g:i a")."\n";

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

BIN
images/agg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
images/misc/dj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

BIN
images/rock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

4342
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,33 +5,34 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config=webpack.config.js --mode=production", "build": "webpack --config=webpack.config.js --mode=production && webpack --config=webpack-reader.config.js --mode=production",
"build-watch": "webpack --config=webpack.config.js --mode=production --watch", "build-watch": "webpack --config=webpack.config.js --mode=production --watch",
"build-dev": "webpack --config=webpack.config.js --mode=development" "build-dev": "webpack --config=webpack.config.js --mode=development",
"build-reader": "webpack --config=webpack-reader.config.js --mode=development"
}, },
"author": "MangaDex", "author": "MangaDex",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@babel/core": "^7.3.3", "@babel/core": "^7.12.10",
"@babel/plugin-transform-runtime": "^7.2.0", "@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.3.1", "@babel/preset-env": "^7.12.11",
"babel-loader": "^8.0.5", "babel-loader": "^8.2.2",
"babel-plugin-transform-class-properties": "^6.24.1", "core-js": "^3.8.2",
"babel-plugin-transform-decorators": "^6.24.1", "webpack": "^4.46.0",
"webpack": "^4.29.5", "webpack-cli": "^3.3.12",
"webpack-cli": "^3.2.3", "webpack-merge": "^4.2.2"
"webpack-merge": "^4.2.1"
}, },
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.2.5", "@babel/runtime": "^7.12.5",
"@babel/runtime": "^7.3.1", "abortcontroller-polyfill": "^1.7.1",
"date-fns": "^2.0.0-alpha.27", "date-fns": "^2.16.1",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.8.0",
"eligrey-classlist-js-polyfill": "^1.2.20180112", "eligrey-classlist-js-polyfill": "^1.2.20180112",
"formdata-polyfill": "^3.0.18", "formdata-polyfill": "^3.0.20",
"polyfill-queryselector": "^1.0.2", "js-cookie": "^2.2.1",
"url-polyfill": "^1.1.3", "natsort": "^2.0.2",
"vtt.js": "^0.13.0", "vtt.js": "^0.13.0",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.5.0",
"wolfy87-eventemitter": "^5.2.9"
} }
} }

3
pages/affiliates.req.php Normal file
View File

@ -0,0 +1,3 @@
<?php
$page_html = parse_template('static_pages/affiliates');

View File

@ -0,0 +1,3 @@
<?php
$page_html = parse_template('user/change_activation_email', ['user' => $user]);

View File

@ -1,6 +1,6 @@
<?php <?php
$manga_lists = new Manga_Lists(); $manga_lists = new Manga_Lists();
$array_of_manga_ids = $manga_lists->get_manga_list(11); $array_of_manga_ids = $manga_lists->get_manga_list(12);
$search = []; $search = [];

View File

@ -441,6 +441,7 @@ else {
", array_keys($blocked_group_ids), 'fetchAll', PDO::FETCH_ASSOC, 60); ", array_keys($blocked_group_ids), 'fetchAll', PDO::FETCH_ASSOC, 60);
} }
$banners = get_banners();
$featured = $memcached->get('featured'); $featured = $memcached->get('featured');
@ -471,6 +472,7 @@ $templateVars = [
'latest_news_posts' => $latest_news_posts, 'latest_news_posts' => $latest_news_posts,
'featured' => $featured, 'featured' => $featured,
'new_manga' => $new_manga, 'new_manga' => $new_manga,
'banners' => $banners,
]; ];
$page_html = parse_template('home', $templateVars); $page_html = parse_template('home', $templateVars);

View File

@ -35,12 +35,17 @@ $manga = new Manga($id);
$relation_types = new Relation_Types(); // This is needed, otherwise it breaks manga.req.js $relation_types = new Relation_Types(); // This is needed, otherwise it breaks manga.req.js
$countryCode = strtoupper(get_country_code($user->last_ip));
if (!isset($manga->manga_id)) { if (!isset($manga->manga_id)) {
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id does not exist."]); $page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id does not exist."]);
} }
elseif (in_array($manga->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) { elseif (in_array($manga->manga_id, RESTRICTED_MANGA_IDS) && !validate_level($user, 'contributor') && $user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA) {
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available. Contact staff on discord for more information."]); $page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available. Contact staff on discord for more information."]);
} }
elseif (isset(REGION_BLOCKED_MANGA[$countryCode]) && in_array($manga->manga_id, REGION_BLOCKED_MANGA[$countryCode]) && !validate_level($user, 'pr')) {
$page_html = parse_template('partials/alert', ['type' => 'danger', 'strong' => 'Warning', 'text' => "Manga #$id is not available."]);
}
else { else {
update_views_v2($page, $manga->manga_id, $ip); update_views_v2($page, $manga->manga_id, $ip);

View File

@ -1,4 +1,7 @@
<?php <?php
use Mangadex\Model\MdexAtHomeClient;
$section = $_GET['section'] ?? 'info'; $section = $_GET['section'] ?? 'info';
$approvaltime = $user->get_client_approval_time(); $approvaltime = $user->get_client_approval_time();
@ -40,6 +43,8 @@ switch ($section) {
'section' => $section, 'section' => $section,
'user_clients' => $user->get_clients(), 'user_clients' => $user->get_clients(),
'approvaltime' => $approvaltime, 'approvaltime' => $approvaltime,
'ip' => _IP,
'backend' => new MdexAtHomeClient(),
]; ];
if (validate_level($user, 'member')) { if (validate_level($user, 'member')) {
@ -54,7 +59,9 @@ switch ($section) {
'user' => $user, 'user' => $user,
'section' => $section, 'section' => $section,
'clients' => $clients, 'clients' => $clients,
]; 'ip' => _IP,
'backend' => new MdexAtHomeClient(),
];
if (validate_level($user, 'admin')) { if (validate_level($user, 'admin')) {
$tab_html = parse_template('md_at_home/partials/admin', $templateVars); $tab_html = parse_template('md_at_home/partials/admin', $templateVars);

View File

@ -34,13 +34,20 @@ switch ($mode) {
default: default:
$deleted = ($mode == 'bin') ? 1 : 0; $deleted = ($mode == 'bin') ? 1 : 0;
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
$limit = 100;
$threads = new PM_Threads($user->user_id, $deleted); $threads = new PM_Threads($user->user_id, $deleted);
$threads_obj = $threads->query_read(); $threads_obj = $threads->query_read($current_page, $limit);
if ($threads->num_rows < 1) { if ($threads->num_rows < 1) {
$messages_tab_html = parse_template('partials/alert', ['type' => 'info', 'strong' => 'Notice', 'text' => 'You have no messages']); $messages_tab_html = parse_template('partials/alert', ['type' => 'info', 'strong' => 'Notice', 'text' => 'There are no messages.']);
} else { } else {
$templateVars = [ $templateVars = [
'thread_count' => $threads->num_rows,
'current_page' => $current_page,
'mode' => $mode,
'limit' => $limit,
'threads' => $threads_obj, 'threads' => $threads_obj,
'user' => $user, 'user' => $user,
'deleted' => $deleted, 'deleted' => $deleted,

View File

@ -1,52 +1,68 @@
<?php <?php
if (!validate_level($user, 'pr')) die('No access'); if (!validate_level($user, 'pr')) die('No access');
$search = []; //pages
$mode = $_GET['mode'] ?? 'banners';
if (isset($_GET['username']) && !empty($_GET['username']))
$search['username'] = trim($_GET['username']);
else
$search['username'] = '';
if (isset($_GET['email']) && !empty($_GET['email']))
$search['email'] = trim($_GET['email']);
else
$search['email'] = '';
$sort = (isset($_GET['s']) && $_GET['s'] > 0) ? $_GET['s'] : 0;
$order = SORT_ARRAY_USERS[$sort];
$limit = $limit ?? 100;
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
if ($search['username'] || $search['email']) {
$users = new Users($search);
$users_obj = $users->query_read($order, $limit, $current_page);
}
else {
$users->num_rows = 0;
$users_obj = new stdClass();
}
$page_html = "";
$templateVars = ['search' => $search];
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
$templateVars = [ $templateVars = [
'search' => $search, 'mode' => $mode,
'sort' => $sort,
'limit' => $limit,
'current_page' => $current_page,
'page' => $page,
'user_list' => $users_obj,
'user_count' => $users->num_rows,
]; ];
if (!$search['username'] && !$search['email']) { $page_html = parse_template('pr/partials/pr_navtabs', $templateVars);
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'No search string']);
} elseif ($users->num_rows < 1) { switch ($mode) {
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'There are no users found with your search criteria.']); case 'email_search':
} else { $search = [];
$page_html .= parse_template('user/user_list', $templateVars);
if (isset($_GET['username']) && !empty($_GET['username']))
$search['username'] = trim($_GET['username']);
else
$search['username'] = '';
if (isset($_GET['email']) && !empty($_GET['email']))
$search['email'] = trim($_GET['email']);
else
$search['email'] = '';
$sort = (isset($_GET['s']) && $_GET['s'] > 0) ? $_GET['s'] : 0;
$order = SORT_ARRAY_USERS[$sort];
$limit = $limit ?? 100;
$current_page = (isset($_GET['p']) && $_GET['p'] > 0) ? $_GET['p'] : 1;
if ($search['username'] || $search['email']) {
$users = new Users($search);
$users_obj = $users->query_read($order, $limit, $current_page);
}
else {
$users->num_rows = 0;
$users_obj = new stdClass();
}
$templateVars = ['search' => $search];
$page_html .= parse_template('pr/partials/user_list_searchbox', $templateVars);
$templateVars = [
'search' => $search,
'sort' => $sort,
'limit' => $limit,
'current_page' => $current_page,
'page' => $page,
'user_list' => $users_obj,
'user_count' => $users->num_rows,
];
if (!$search['username'] && !$search['email']) {
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'No search string']);
} elseif ($users->num_rows < 1) {
$page_html .= parse_template('partials/alert', ['type' => 'info mt-3', 'strong' => 'Notice', 'text' => 'There are no users found with your search criteria.']);
} else {
$page_html .= parse_template('user/user_list', $templateVars);
}
break;
case "banners":
default:
$templateVars['banners'] = get_banners(false);
$page_html .= parse_template('pr/banners', $templateVars);
break;
} }

View File

@ -1,17 +1,42 @@
<?php <?php
$transactions = $user->get_transactions(); $mode = $_GET['mode'] ?? 'home';
if ($transactions) { $templateVars = [
$sql->modify('claim_transaction', ' UPDATE mangadex_user_transactions SET user_id = ? WHERE user_id = 0 AND email LIKE ? ', [$user->user_id, $transactions[0]['email']]); 'mode' => $mode
];
$memcached->delete("user_{$user->user_id}_transactions"); $page_html = parse_template('support/partials/support_navtabs', $templateVars);
switch ($mode) {
case 'home':
$page_html .= parse_template('support/home', [
'user' => $user
]);
break;
case 'donate':
$wallet_no = substr($user->user_id, -1);
$wallet_no_2 = floor(substr($user->user_id, -1) / 2);
$page_html .= parse_template('support/donate', [
'wallet_no' => $wallet_no,
'wallet_no_2' => $wallet_no_2
]);
break;
case 'history':
$transactions = $user->get_transactions();
if ($transactions) {
$sql->modify('claim_transaction', ' UPDATE mangadex_user_transactions SET user_id = ? WHERE user_id = 0 AND email LIKE ? ', [$user->user_id, $transactions[0]['email']]);
$memcached->delete("user_{$user->user_id}_transactions");
}
$page_html .= parse_template('support/history', [
'user' => $user
]);
break;
case 'affiliates':
$page_html .= parse_template('support/affiliates', $templateVars);
break;
} }
$wallet_no = substr($user->user_id, -1);
$wallet_no_2 = floor(substr($user->user_id, -1) / 2);
$page_html = parse_template('user/support', [
'user' => $user,
'wallet_no' => $wallet_no,
'wallet_no_2' => $wallet_no_2,
]);

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,41 @@
<?php <?php
class Cache extends Memcached class Synced_Memcached extends Memcached
{
private $memcachedSync = null;
public function __construct($persistent_id = '', $on_new_object_cb = null, $connection_str = '')
{
parent::__construct($persistent_id, $on_new_object_cb, $connection_str);
if (defined('MEMCACHED_SYNC_HOST') && !empty(MEMCACHED_SYNC_HOST)) {
$this->memcachedSync = new Memcached('sync_host');
if (!$this->memcachedSync->getServerList()) {
// Persistent servers remember the serverlist, so only add if its empty after a php-fpm restart
$this->memcachedSync->addServer(MEMCACHED_SYNC_HOST, defined('MEMCACHED_SYNC_PORT') ? MEMCACHED_SYNC_PORT : 11211);
}
}
}
public function setSynced($key, $value, $expiration = 0, $udf_flags = 0)
{
parent::set($key, $value, $expiration);
if ($this->memcachedSync !== null) {
$this->memcachedSync->set($key, $value, $expiration);
}
}
public function deleteSynced($key, $time = 0)
{
parent::delete($key, $time);
if ($this->memcachedSync !== null) {
$this->memcachedSync->delete($key, $time);
}
}
}
class Cache extends Synced_Memcached
{ {
const RESULT_CODES = [ const RESULT_CODES = [
@ -76,7 +111,7 @@ class Cache extends Memcached
public function get($key, $cache_cb = null, $flags = null) public function get($key, $cache_cb = null, $flags = null)
{ {
$res = parent::get($key, $cache_cb, $flags); // TODO: Change the autogenerated stub $res = parent::get($key, $cache_cb, $flags);
$this->stats[$res === false ? 'miss' : 'hit']++; $this->stats[$res === false ? 'miss' : 'hit']++;
$this->log[] = [ $this->log[] = [
'method' => "GET", 'method' => "GET",
@ -88,7 +123,7 @@ class Cache extends Memcached
return $res; return $res;
} }
public function set($key, $value, $expiration = 0) public function set($key, $value, $expiration = 0, $udf_flags = 0)
{ {
parent::set($key, $value, $expiration); parent::set($key, $value, $expiration);
$this->stats['set']++; $this->stats['set']++;
@ -103,8 +138,7 @@ class Cache extends Memcached
public function delete($key, $time = 0) public function delete($key, $time = 0)
{ {
parent::delete($key, $time);
parent::delete($key, $time); // TODO: Change the autogenerated stub
$this->stats['delete']++; $this->stats['delete']++;
$this->log[] = [ $this->log[] = [
'method' => "DEL", 'method' => "DEL",

View File

@ -102,7 +102,7 @@ class Chapters {
$limit = prepare_numeric($limit); $limit = prepare_numeric($limit);
$offset = prepare_numeric($limit * ($current_page - 1)); $offset = prepare_numeric($limit * ($current_page - 1));
$results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_".md5($orderby)."_offset_$offset", " $results = $this->sql->prep("chapters_query_" . hash_array($this->pdo_bind) . "_orderby_" . md5($orderby) . "_offset_$offset" . "_limit_$limit", "
SELECT chapters.*, SELECT chapters.*,
lang.*, lang.*,
users.username, users.username,

View File

@ -182,7 +182,7 @@ class User {
SELECT count(*) SELECT count(*)
FROM mangadex_pm_threads FROM mangadex_pm_threads
WHERE (sender_id = ? AND sender_read = 0) OR (recipient_id = ? AND recipient_read = 0) WHERE (sender_id = ? AND sender_read = 0) OR (recipient_id = ? AND recipient_read = 0)
", [$this->user_id, $this->user_id], 'fetchColumn', '', -1); ", [$this->user_id, $this->user_id], 'fetchColumn', '', 60);
} }
public function get_unread_notifications() { public function get_unread_notifications() {
@ -190,7 +190,7 @@ class User {
SELECT count(*) SELECT count(*)
FROM mangadex_notifications FROM mangadex_notifications
WHERE mentionee_user_id = ? AND is_read = 0 WHERE mentionee_user_id = ? AND is_read = 0
", [$this->user_id], 'fetchColumn', ''); ", [$this->user_id], 'fetchColumn', '', 60);
} }
public function get_groups() { public function get_groups() {
@ -236,21 +236,19 @@ class User {
", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE); //contains progress tracker (volume and chapter) ", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE); //contains progress tracker (volume and chapter)
} }
public function get_manga_userdata($manga_id) { //contains progress data, title, and rating public function get_manga_userdata($manga_id) { //contains progress data, title, and rating
return $this->sql->prep("user_{$this->user_id}_manga_{$manga_id}_api", " $follows = $this->get_followed_manga_ids_api();
SELECT m.manga_id, m.manga_name AS title, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating foreach ($follows as $manga) {
FROM mangadex_mangas m if ($manga['manga_id'] == $manga_id) {
LEFT JOIN mangadex_follow_user_manga f return $manga;
ON m.manga_id = f.manga_id AND f.user_id = ? }
LEFT JOIN mangadex_manga_ratings r }
ON m.manga_id = r.manga_id AND r.user_id = ? return null;
WHERE m.manga_id = ?
", [$this->user_id, $this->user_id, $manga_id], 'fetchAll', PDO::FETCH_ASSOC);
} }
public function get_followed_manga_ids_api() { //contains progress data, title, and rating for all followed manga public function get_followed_manga_ids_api() { //contains progress data, title, and rating for all followed manga
return $this->sql->prep("user_{$this->user_id}_followed_manga_ids_api", " return $this->sql->prep("user_{$this->user_id}_followed_manga_ids_api", "
SELECT f.manga_id, m.manga_name AS title, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating SELECT f.manga_id, m.manga_name AS title, m.manga_hentai, m.manga_image, f.follow_type, f.volume, f.chapter, COALESCE(r.rating, 0) as rating
FROM mangadex_follow_user_manga f FROM mangadex_follow_user_manga f
JOIN mangadex_mangas m JOIN mangadex_mangas m
ON m.manga_id = f.manga_id ON m.manga_id = f.manga_id
@ -330,7 +328,7 @@ class User {
return $this->sql->prep("user_{$this->user_id}_friends_user_ids", " return $this->sql->prep("user_{$this->user_id}_friends_user_ids", "
SELECT relations.target_user_id, relations.accepted, user.user_id, user.username, user.last_seen_timestamp, user.list_privacy, user_level.level_colour SELECT relations.target_user_id, relations.accepted, user.user_id, user.username, user.last_seen_timestamp, user.list_privacy, user_level.level_colour
FROM mangadex_user_relations AS relations FROM mangadex_user_relations AS relations
LEFT JOIN mangadex_users AS user JOIN mangadex_users AS user
ON relations.target_user_id = user.user_id ON relations.target_user_id = user.user_id
LEFT JOIN mangadex_user_levels AS user_level LEFT JOIN mangadex_user_levels AS user_level
ON user.level_id = user_level.level_id ON user.level_id = user_level.level_id
@ -343,26 +341,26 @@ class User {
return $this->sql->prep("user_{$this->user_id}_pending_friends_user_ids", " return $this->sql->prep("user_{$this->user_id}_pending_friends_user_ids", "
SELECT relations.user_id, user.user_id, user.username, user.last_seen_timestamp, user_level.level_colour SELECT relations.user_id, user.user_id, user.username, user.last_seen_timestamp, user_level.level_colour
FROM mangadex_user_relations AS relations FROM mangadex_user_relations AS relations
LEFT JOIN mangadex_users AS user JOIN mangadex_users AS user
ON relations.user_id = user.user_id ON relations.user_id = user.user_id
LEFT JOIN mangadex_user_levels AS user_level LEFT JOIN mangadex_user_levels AS user_level
ON user.level_id = user_level.level_id ON user.level_id = user_level.level_id
WHERE relations.relation_id = 1 AND relations.accepted = 0 AND relations.target_user_id = ? WHERE relations.relation_id = 1 AND relations.accepted = 0 AND relations.target_user_id = ?
ORDER BY user.username ASC ORDER BY user.username ASC
", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE); ", [$this->user_id], 'fetchAll', PDO::FETCH_UNIQUE, 60*60*24);
} }
public function get_blocked_user_ids() { public function get_blocked_user_ids() {
return $this->sql->prep("user_{$this->user_id}_blocked_user_ids", " return $this->sql->prep("user_{$this->user_id}_blocked_user_ids", "
SELECT relations.target_user_id, user.user_id, user.username, user_level.level_colour SELECT relations.target_user_id, user.user_id, user.username, user_level.level_colour
FROM mangadex_user_relations AS relations FROM mangadex_user_relations AS relations
LEFT JOIN mangadex_users AS user JOIN mangadex_users AS user
ON relations.target_user_id = user.user_id ON relations.target_user_id = user.user_id
LEFT JOIN mangadex_user_levels AS user_level LEFT JOIN mangadex_user_levels AS user_level
ON user.level_id = user_level.level_id ON user.level_id = user_level.level_id
WHERE relations.relation_id = 0 AND relations.user_id = ? AND user.level_id < ? WHERE relations.relation_id = 0 AND relations.user_id = ? AND user.level_id < ?
ORDER BY user.username ASC ORDER BY user.username ASC
", [$this->user_id, 10 /** staff level: PR **/], 'fetchAll', PDO::FETCH_UNIQUE); ", [$this->user_id, 10 /** staff level: PR **/], 'fetchAll', PDO::FETCH_UNIQUE, 60*60*24);
} }
public function get_active_restrictions() { public function get_active_restrictions() {
@ -515,15 +513,18 @@ class PM_Threads {
FROM mangadex_pm_threads FROM mangadex_pm_threads
WHERE (sender_id = ? AND sender_deleted = ?) WHERE (sender_id = ? AND sender_deleted = ?)
OR (recipient_id = ? AND recipient_deleted = ?) OR (recipient_id = ? AND recipient_deleted = ?)
ORDER BY thread_timestamp DESC LIMIT 20
", [$user_id, $deleted, $user_id, $deleted], 'fetchColumn', '', -1); ", [$user_id, $deleted, $user_id, $deleted], 'fetchColumn', '', -1);
$this->user_id = $user_id; $this->user_id = $user_id;
$this->deleted = $deleted; $this->deleted = $deleted;
} }
public function query_read() { public function query_read($page = 1, $limit = 100)
$results = $this->sql->prep("user_{$this->user_id}_PMs", " {
$offset = ($page - 1) * $limit;
$results = $this->sql->prep(
"user_{$this->user_id}_PMs",
"
SELECT threads.*, SELECT threads.*,
sender.username AS sender_username, sender.username AS sender_username,
recipient.username AS recipient_username, recipient.username AS recipient_username,
@ -541,8 +542,13 @@ class PM_Threads {
WHERE (threads.sender_id = ? AND threads.sender_deleted = ?) WHERE (threads.sender_id = ? AND threads.sender_deleted = ?)
OR (threads.recipient_id = ? AND threads.recipient_deleted = ?) OR (threads.recipient_id = ? AND threads.recipient_deleted = ?)
ORDER BY threads.thread_timestamp DESC ORDER BY threads.thread_timestamp DESC
LIMIT 100 LIMIT ? OFFSET ?
", [$this->user_id, $this->deleted, $this->user_id, $this->deleted], 'fetchAll', PDO::FETCH_ASSOC, -1); ",
[$this->user_id, $this->deleted, $this->user_id, $this->deleted, $limit, $offset],
'fetchAll',
PDO::FETCH_ASSOC,
-1
);
//return get_results_as_object($results, 'thread_id'); //return get_results_as_object($results, 'thread_id');
return $results; return $results;

View File

@ -19,7 +19,6 @@
border-radius: 5px; border-radius: 5px;
} }
.noselect { .noselect {
-webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
-epub-user-select: none; -epub-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -29,11 +28,6 @@
} }
.nodrag { .nodrag {
-webkit-user-drag: none; -webkit-user-drag: none;
-epub-user-drag: none;
-moz-user-drag: none;
-ms-user-drag: none;
-o-user-drag: none;
user-drag: none;
} }
.noevents { .noevents {
-webkit-pointer-events: none; -webkit-pointer-events: none;
@ -70,12 +64,14 @@ body {
.reader-page-bar, .reader-page-bar,
.reader-page-bar .trail, .reader-page-bar .trail,
.reader-page-bar .thumb, .reader-page-bar .thumb,
.reader-page-bar .track { .reader-page-bar .track,
.reader-controls-collapser:before,
.reader-controls-collapser span {
transition-property: all; transition-property: all;
transition-duration: 0.2s; transition-duration: 0.2s;
} }
#reader-controls-collapser span { #reader-controls-collapser-bar {
transition-property: all; transition-property: opacity;
transition-duration: 0.4s; transition-duration: 0.4s;
} }
@ -88,18 +84,12 @@ nav.navbar {
.footer { .footer {
border-top: 1px solid rgba(128, 128, 128, 0.5); border-top: 1px solid rgba(128, 128, 128, 0.5);
} }
#reader-controls-collapser {
border-left: 1px solid rgba(128, 128, 128, 0.5);
border-right: 1px solid rgba(128, 128, 128, 0.5);
}
/* settings and controls */ /* settings and controls */
#modal-settings:not(.show-advanced) .advanced { #modal-settings:not(.show-advanced) .advanced {
display: none; display: none;
} }
#modal-settings .advanced label {
}
#modal-settings .advanced label:before { #modal-settings .advanced label:before {
content: '* '; content: '* ';
} }
@ -110,12 +100,70 @@ nav.navbar {
left: 0; left: 0;
max-width: 100vw; max-width: 100vw;
} }
#reader-controls-collapser { .reader-controls-collapser {
width: 34px; display: none;
color: #eee;
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
} }
.reader.hide-sidebar #reader-controls-collapser .fa-caret-right { .reader-controls-collapser:hover {
color: #fff;
}
#reader-controls-collapser-button {
position: absolute;
width: 2.75rem;
height: 2.75rem;
}
#reader-controls-collapser-button > span {
position: absolute;
z-index: 100;
margin-right: 1rem;
margin-bottom: 0.7rem;
}
#reader-controls-collapser-button:before {
content: "";
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 2.75rem 2.75rem 0 0;
border-color: rgba(128, 128, 128, 0.2) transparent transparent transparent;
}
#reader-controls-collapser-button:hover:before {
border-color: rgba(128, 128, 128, 0.4) transparent transparent transparent;
}
.reader.hide-sidebar #reader-controls-collapser-button {
position: fixed;
z-index: 99;
top: 3.5rem;
right: 0;
}
.reader.hide-sidebar.hide-header #reader-controls-collapser-button {
top: 0;
}
.reader.hide-sidebar #reader-controls-collapser-button {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
#reader-controls-collapser-bar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
margin-left: -40px;
width: 40px;
background: linear-gradient(to right, rgba(64, 64, 64, 0), rgba(64, 64, 64, 0.4));
opacity: 0;
}
#reader-controls-collapser-bar:hover {
opacity: 1;
}
.reader.hide-sidebar #reader-controls-collapser-bar span {
transform: rotateY(180deg);
}
#reader-controls-collapser-bar span {
font-size: 2.75rem;
margin-left: 0.3rem;
}
.reader-controls-mode span:not(.fas) { .reader-controls-mode span:not(.fas) {
display: none display: none
} }
@ -333,9 +381,6 @@ body {
.reader-page-bar:hover .thumb { .reader-page-bar:hover .thumb {
background: #eee; background: #eee;
} }
.reader-page-bar:hover .track {
/*border: 2px solid #ccc;*/
}
.reader-page-bar .notch:not(.loaded) { .reader-page-bar .notch:not(.loaded) {
background: repeating-linear-gradient( background: repeating-linear-gradient(
-45deg, -45deg,
@ -431,6 +476,11 @@ body {
min-height: 100%; min-height: 100%;
} }
/* cursor hiding */
.hide-cursor .reader-images img {
cursor: none;
}
/* Modernizr */ /* Modernizr */
.no-localstorage #alert-storage-warning { .no-localstorage #alert-storage-warning {
@ -454,6 +504,15 @@ body {
/* desktop definitions */ /* desktop definitions */
@media (min-width: 992px) { @media (min-width: 992px) {
.reader-controls {
border-left: 1px solid rgba(128, 128, 128, 0.5);
}
.reader-controls-title {
padding-left: 0 !important;
}
.reader-controls-title .manga-title-col {
padding: 0 2.75rem;
}
.reader.layout-horizontal .reader-controls-wrapper { .reader.layout-horizontal .reader-controls-wrapper {
order: 2; order: 2;
@ -462,6 +521,10 @@ body {
order: 1; order: 1;
} }
#right_swipe_area {
display: none;
}
/* controls */ /* controls */
.reader:not(.layout-horizontal) .d-lg-none { .reader:not(.layout-horizontal) .d-lg-none {
@ -469,7 +532,7 @@ body {
} }
.reader:not(.layout-horizontal) .reader-controls-pages, .reader:not(.layout-horizontal) .reader-controls-pages,
.reader:not(.layout-horizontal) .reader-controls-footer, .reader:not(.layout-horizontal) .reader-controls-footer,
.reader:not(.layout-horizontal) #reader-controls-collapser .reader:not(.layout-horizontal) .reader-controls-collapser
{ {
display: none !important; display: none !important;
} }
@ -486,12 +549,17 @@ body {
top: 0; top: 0;
} }
.reader.layout-horizontal.hide-sidebar .reader-controls-wrapper { .reader.layout-horizontal.hide-sidebar .reader-controls-wrapper {
width: 34px; width: 0;
} }
.reader.layout-horizontal.hide-sidebar .reader-controls { .reader.layout-horizontal.hide-sidebar .reader-controls {
overflow: hidden; overflow: hidden;
} }
.reader[data-collapser="bar"] #reader-controls-collapser-bar,
.reader[data-collapser="button"] #reader-controls-collapser-button {
display: flex;
}
/* load icon */ /* load icon */
.reader.layout-horizontal .reader-load-icon { .reader.layout-horizontal .reader-load-icon {
@ -511,7 +579,7 @@ body {
width: auto; width: auto;
} }
.reader.layout-horizontal.hide-sidebar .reader-page-bar { .reader.layout-horizontal.hide-sidebar .reader-page-bar {
right: 34px; right: 0;
} }
.reader.layout-horizontal .reader-page-bar:hover .track { .reader.layout-horizontal .reader-page-bar:hover .track {
height: 50px; height: 50px;
@ -532,7 +600,8 @@ body {
padding-right: 20vw !important; padding-right: 20vw !important;
} }
.reader.layout-horizontal.hide-sidebar .reader-images { .reader.layout-horizontal.hide-sidebar .reader-images {
padding-right: 34px !important; padding-right: 0 !important;
} }
} }

View File

@ -11,9 +11,9 @@ body {
font-family: "Ubuntu", sans-serif; font-family: "Ubuntu", sans-serif;
} }
.flag{display:inline-block;background:url(/images/flags-flat-24.png) no-repeat;background-size:100%;width:24px;min-width:24px;height:24px;vertical-align:bottom;opacity:0.75;} .flag{display:inline-block;background:url(/images/flags-flat-24-20201130.png) no-repeat;background-size:100%;width:24px;min-width:24px;height:24px;vertical-align:bottom;opacity:0.75;}
.flag-_unknown{background-position:0 -0px}.flag-bd{background-position:0 -24px}.flag-bg{background-position:0 -48px}.flag-br{background-position:0 -72px}.flag-cn{background-position:0 -96px}.flag-ct{background-position:0 -120px}.flag-cz{background-position:0 -144px}.flag-de{background-position:0 -168px}.flag-dk{background-position:0 -192px}.flag-es{background-position:0 -216px}.flag-fi{background-position:0 -240px}.flag-fr{background-position:0 -264px}.flag-gb{background-position:0 -288px}.flag-gr{background-position:0 -312px}.flag-hk{background-position:0 -336px}.flag-hu{background-position:0 -360px}.flag-id{background-position:0 -384px}.flag-il{background-position:0 -408px}.flag-in{background-position:0 -432px}.flag-ir{background-position:0 -456px}.flag-it{background-position:0 -480px}.flag-jp{background-position:0 -504px}.flag-kr{background-position:0 -528px}.flag-lt{background-position:0 -552px}.flag-mm{background-position:0 -576px}.flag-mn{background-position:0 -600px}.flag-mx{background-position:0 -624px}.flag-my{background-position:0 -648px}.flag-nl{background-position:0 -672px}.flag-no{background-position:0 -696px}.flag-ph{background-position:0 -720px}.flag-pl{background-position:0 -744px}.flag-pt{background-position:0 -768px}.flag-ro{background-position:0 -792px}.flag-rs{background-position:0 -816px}.flag-ru{background-position:0 -840px}.flag-sa{background-position:0 -864px}.flag-se{background-position:0 -888px}.flag-th{background-position:0 -912px}.flag-tr{background-position:0 -936px}.flag-ua{background-position:0 -960px}.flag-vn{background-position:0 -984px} .flag-_unknown{background-position:0 -0px}.flag-bd{background-position:0 -24px}.flag-bg{background-position:0 -48px}.flag-br{background-position:0 -72px}.flag-cn{background-position:0 -96px}.flag-ct{background-position:0 -120px}.flag-cz{background-position:0 -144px}.flag-de{background-position:0 -168px}.flag-dk{background-position:0 -192px}.flag-es{background-position:0 -216px}.flag-fi{background-position:0 -240px}.flag-fr{background-position:0 -264px}.flag-gb{background-position:0 -288px}.flag-gr{background-position:0 -312px}.flag-hk{background-position:0 -336px}.flag-hu{background-position:0 -360px}.flag-id{background-position:0 -384px}.flag-il{background-position:0 -408px}.flag-in{background-position:0 -432px}.flag-ir{background-position:0 -456px}.flag-it{background-position:0 -480px}.flag-jp{background-position:0 -504px}.flag-kr{background-position:0 -528px}.flag-kz{background-position:0 -552px}.flag-lt{background-position:0 -576px}.flag-mm{background-position:0 -600px}.flag-mn{background-position:0 -624px}.flag-mx{background-position:0 -648px}.flag-my{background-position:0 -672px}.flag-nl{background-position:0 -696px}.flag-no{background-position:0 -720px}.flag-ph{background-position:0 -744px}.flag-pl{background-position:0 -768px}.flag-pt{background-position:0 -792px}.flag-ro{background-position:0 -816px}.flag-rs{background-position:0 -840px}.flag-ru{background-position:0 -864px}.flag-sa{background-position:0 -888px}.flag-se{background-position:0 -912px}.flag-th{background-position:0 -936px}.flag-tr{background-position:0 -960px}.flag-ua{background-position:0 -984px}.flag-vn{background-position:0 -1008px}
.badge { .badge {
user-select: none; user-select: none;

View File

@ -529,12 +529,6 @@ function display_manga_ext_links($links_array) {
} }
} }
function display_manga_logo_link($manga) {
return "<a alt='Manga $manga->manga_id' title='$manga->manga_name' href='/title/$manga->manga_id/" . slugify($manga->manga_name) . "'>
<img class='rounded' src='" . IMG_SERVER_URL . "/images/manga/$manga->manga_id.thumb.jpg' alt='Manga image' />
</a>";
}
function display_js_posting() { function display_js_posting() {
return " return "
$('.bbcode').click(function(){ $('.bbcode').click(function(){
@ -640,7 +634,7 @@ function display_forum($forum, $user) {
$return = " $return = "
<div class='d-flex row m-0 py-1 border-bottom align-items-center'> <div class='d-flex row m-0 py-1 border-bottom align-items-center'>
<div class='col-auto px-2 ' > <div class='col-auto px-2 ' >
<a href='/forum/$forum->forum_id'><img src='" . LOCAL_SERVER_URL . "/images/forums/$forum->forum_name.svg' width='70px' ></a> <a href='/forum/$forum->forum_id'><img src='" . LOCAL_SERVER_URL . "/images/forums/" . str_replace(' ', '-', $forum->forum_name) . ".svg' width='70px' ></a>
</div> </div>
<div class='col p-0 text-truncate'> <div class='col p-0 text-truncate'>
<div class='row m-2'> <div class='row m-2'>
@ -1520,6 +1514,12 @@ function display_lock_manga($user, $manga) {
} }
} }
function display_regenerate_manga_thumb($user) {
if (validate_level($user, 'mod')) {
return "<button class='btn btn-info' id='manga_regenerate_thumb_button'>" . display_fa_icon('sync', 'Regenerate thumb') . " <span class='d-none d-xl-inline'>Regenerate thumb</span></button>";
}
}
function display_delete_manga($user) { function display_delete_manga($user) {
if (validate_level($user, 'admin')) if (validate_level($user, 'admin'))
return "<button class='btn btn-danger float-right' id='delete_button'>" . display_fa_icon('trash', 'Delete') . " <span class='d-none d-xl-inline'>Delete</span></button>"; return "<button class='btn btn-danger float-right' id='delete_button'>" . display_fa_icon('trash', 'Delete') . " <span class='d-none d-xl-inline'>Delete</span></button>";

View File

@ -998,6 +998,24 @@ function get_zip_originalsize($filename) {
return $size; return $size;
} }
function get_banners($enabledOnly = true){
global $sql;
$query = "
SELECT banner_id, banners.user_id, username, ext, is_enabled, is_anonymous, levels.level_name, levels.level_colour
FROM mangadex_banners banners
JOIN mangadex_users users
ON banners.user_id = users.user_id
JOIN mangadex_user_levels levels
ON users.level_id = levels.level_id
";
if($enabledOnly){
$query .= " WHERE is_enabled = 1";
}
$banners = $sql->prep("banners_" . ($enabledOnly ? "enabled" : "all"), $query, [], 'fetchAll', PDO::FETCH_ASSOC, 600);
return $banners;
}
/************************************* /*************************************
* Discord webhook * Discord webhook
*************************************/ *************************************/

View File

@ -2,17 +2,17 @@
// Sentry error handling (must be as early as possible to catch any init exceptions= // Sentry error handling (must be as early as possible to catch any init exceptions=
if (defined('SENTRY_DSN') && SENTRY_DSN && class_exists('Raven_Client')) { if (defined('SENTRY_DSN') && SENTRY_DSN && class_exists('Raven_Client')) {
$sentry = new Raven_Client(SENTRY_DSN, [ $sentry = new Raven_Client(SENTRY_DSN, [
'sample_rate' => SENTRY_SAMPLE_RATE, 'sample_rate' => SENTRY_SAMPLE_RATE,
'curl_method' => SENTRY_CURL_METHOD, 'curl_method' => SENTRY_CURL_METHOD,
'timeout' => SENTRY_TIMEOUT 'timeout' => SENTRY_TIMEOUT
]); ]);
try { try {
$sentry->install(); $sentry->install();
} catch (\Raven_Exception $e) { } catch (\Raven_Exception $e) {
// This should land in the logfiles at least but not block script execution // This should land in the logfiles at least but not block script execution
trigger_error('Failed to install Sentry client: '.$e->getMessage(), E_USER_WARNING); trigger_error('Failed to install Sentry client: '.$e->getMessage(), E_USER_WARNING);
} }
} }
//database stuff //database stuff
@ -24,217 +24,249 @@ $dsn_master = "mysql:host=$host;dbname=$db;charset=$charset";
$dsn_slaves = []; $dsn_slaves = [];
foreach (DB_READ_HOSTS ?? [] AS $slave_host) { foreach (DB_READ_HOSTS ?? [] AS $slave_host) {
$slave_db = DB_READ_NAME; $slave_db = DB_READ_NAME;
$slave_port = 3306; $slave_port = 3306;
if (strpos($slave_host, ':') !== false) { if (strpos($slave_host, ':') !== false) {
[$slave_host, $slave_port] = explode(':', $slave_host, 2); [$slave_host, $slave_port] = explode(':', $slave_host, 2);
} }
$dsn_slaves[] = "mysql:host=$slave_host;port=$slave_port;dbname=$slave_db;charset=$charset"; $dsn_slaves[] = "mysql:host=$slave_host;port=$slave_port;dbname=$slave_db;charset=$charset";
} }
$opt = [ $opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => defined('DB_PERSISTENT') ? (bool)DB_PERSISTENT : false,
]; ];
class SQL extends PDO { class SQL extends PDO {
private $debug = []; private $debug = [];
private $time_array = []; private $time_array = [];
/** @var \PDO */ /** @var \PDO */
private $slave_sql; private $slave_sql;
public function __construct(string $dsn_master, array $dsn_slaves, $username = null, $passwd = null, $options = null) private $credentials = [];
{ private $isConnected = false;
// Establish connection with master
parent::__construct($dsn_master, $username, $passwd, $options);
// Randomize pick order
shuffle($dsn_slaves);
while (!empty($dsn_slaves)) {
$dsn_slave = array_pop($dsn_slaves);
$error = 'Slave failed with unknown reason';
try {
$this->slave_sql = new \PDO($dsn_slave, DB_READ_USER, DB_READ_PASSWORD, $options);
// Try a ping
if (false === $this->slave_sql->query('SELECT 1')) {
throw new \RuntimeException('Ping on slave failed!');
}
// Connection successful
return;
} catch (\PDOException $e) {
$error = sprintf('Slave failed with error code %s', $e->getCode());
} catch (\Throwable $e) {
$error = sprintf('Unexpected exception: %s', $e->getMessage());
}
// A slave failed, report warning to sentry
trigger_error($error, E_USER_WARNING);
}
// Fall back to master
$this->slave_sql = $this;
}
public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
global $memcached;
$name = str_replace(' ', '_', $name);
if ($expiry < 0) //delete from cache and update
$memcached->delete($name);
$start = microtime(true);
$cache = $memcached->get($name);
$from_cache = 'Y';
if ($cache === FALSE) {
if ($fetch == 'fetchAll')
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
elseif ($fetch == 'fetchColumn')
$cache = $this->slave_sql->query($query)->fetchColumn();
else
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
if ($expiry >= 0)
$memcached->set($name, $cache, $expiry);
$from_cache = 'N';
}
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
return $cache;
}
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0) {
global $memcached;
$name = str_replace(' ', '_', $name);
if ($expiry < 0) //delete from cache and update
$memcached->delete($name);
$start = microtime(true);
$cache = $memcached->get($name);
$from_cache = 'Y';
if ($cache === FALSE) {
$stmt = $this->slave_sql->prepare($query);
$stmt->execute($bind);
switch ($fetch) {
case 'fetch':
$cache = $stmt->fetch($pdo_mode);
break;
case 'fetchColumn':
$cache = $stmt->fetchColumn();
break;
default:
$cache = $stmt->fetchAll($pdo_mode);
break;
}
if ($expiry >= 0)
$memcached->set($name, $cache, $expiry);
$from_cache = 'N';
}
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
return $cache;
}
public function modify($name, $query, $bind) {
$name = str_replace(' ', '_', $name);
$start = microtime(true);
$from_cache = '/';
$stmt = $this->prepare($query);
$stmt->execute($bind);
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
$this->debug[] = [$name, nl2br(trim($query)), 'modify', $from_cache, $time_taken];
return $this->lastInsertId();
}
public function modify_deferred($name, $query, $bind) {
public function __construct(string $dsn_master, array $dsn_slaves, $username = null, $passwd = null, $options = null)
{
$this->credentials = [
'dsn_master' => $dsn_master,
'dsn_slaves' => $dsn_slaves,
'username' => $username,
'passwd' => $passwd,
'options' => $options,
];
} }
public function debug() { private function ensureConnected(): void
global $memcached; {
if (!$this->isConnected) {
$dsn_master = $this->credentials['dsn_master'];
$dsn_slaves = $this->credentials['dsn_slaves'];
$username = $this->credentials['username'];
$passwd = $this->credentials['passwd'];
$options = $this->credentials['options'];
$this->credentials = [];
// ======== Add sql table // Establish connection with master
$return = " parent::__construct($dsn_master, $username, $passwd, $options);
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
<tr>
<th><button id='toggle-sql-table'><i class='fa fa-eye'></i></button></th>
<th>N</th>
<th>Q</th>
<th>M</th>
<th>C</th>
<th>T</th>
</tr>";
foreach ($this->debug as $key => $array) { $this->isConnected = true;
++$key;
$return .= "<tr style='display:none'>";
$return .= "<td>$key</td>";
foreach ($array as $value) {
$return .= "<td>$value</td>";
}
$return .= "</tr>";
}
$total_time = array_sum($this->time_array); // Randomize pick order
shuffle($dsn_slaves);
$return .= " while (!empty($dsn_slaves)) {
<tr style='display:none'> $dsn_slave = array_pop($dsn_slaves);
<th>#</th> $error = 'Slave failed with unknown reason';
<th>Name</th> try {
<th>Query</th> $this->slave_sql = new \PDO($dsn_slave, DB_READ_USER, DB_READ_PASSWORD, $options);
<th>Mode</th> // Try a ping
<th>Cache</th> if (false === $this->slave_sql->query('SELECT 1')) {
<th>$total_time</th> throw new \RuntimeException('Ping on slave failed!');
</tr> }
</table>"; // Connection successful
return;
} catch (\PDOException $e) {
$error = sprintf('Slave failed with error code %s', $e->getCode());
} catch (\Throwable $e) {
$error = sprintf('Unexpected exception: %s', $e->getMessage());
}
// A slave failed, report warning to sentry
trigger_error($error, E_USER_WARNING);
}
// Fall back to master
$this->slave_sql = $this;
}
}
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) { public function query_read($name, $query, $fetch, $pdo_mode, $expiry = 0) {
global $memcached;
$name = str_replace(' ', '_', $name);
if ($expiry < 0) {
$memcached->delete($name);
}
$start = microtime(true);
$cache = $memcached->get($name);
$from_cache = 'Y';
if ($cache === FALSE) {
$this->ensureConnected();
if ($fetch === 'fetchAll') {
$cache = $this->slave_sql->query($query)->fetchAll($pdo_mode);
}
elseif ($fetch === 'fetchColumn') {
$cache = $this->slave_sql->query($query)->fetchColumn();
}
else {
$cache = $this->slave_sql->query($query)->fetch($pdo_mode);
}
if ($expiry >= 0) {
$memcached->set($name, $cache, $expiry);
}
$from_cache = 'N';
}
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
return $cache;
}
public function prep($name, $query, $bind, $fetch, $pdo_mode = '', $expiry = 0, $force_master = false) {
global $memcached;
$name = str_replace(' ', '_', $name);
if ($expiry < 0) {
$memcached->delete($name);
}
$start = microtime(true);
$cache = $memcached->get($name);
$from_cache = 'Y';
if ($cache === FALSE) {
$this->ensureConnected();
$stmt = $force_master ? $this->prepare($query) : $this->slave_sql->prepare($query);
$stmt->execute($bind);
switch ($fetch) {
case 'fetch':
$cache = $stmt->fetch($pdo_mode);
break;
case 'fetchColumn':
$cache = $stmt->fetchColumn();
break;
default:
$cache = $stmt->fetchAll($pdo_mode);
break;
}
if ($expiry >= 0) {
$memcached->set($name, $cache, $expiry);
}
$from_cache = 'N';
}
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
$this->debug[] = [$name, nl2br(trim($query)), $fetch, $from_cache, $time_taken];
return $cache;
}
public function modify($name, $query, $bind) {
$name = str_replace(' ', '_', $name);
$start = microtime(true);
$from_cache = '/';
$this->ensureConnected();
$stmt = $this->prepare($query);
$stmt->execute($bind);
$time_taken = round((microtime(true) - $start) * 1000, 2);
$this->time_array[] = $time_taken;
$query = preg_replace(array_fill(0, count($bind), '/\?/'), array_fill(0, count($bind), "<span style='color: red'>~</span>"), $query, 1);
$query = preg_replace(array_fill(0, count($bind), '/~/'), $bind, $query, 1);
$this->debug[] = [$name, nl2br(trim($query)), 'modify', $from_cache, $time_taken];
return $this->lastInsertId();
}
public function debug() {
global $memcached;
// ======== Add sql table
$return = "
<table style='margin-top: 50px;' class='table table-condensed table-striped'>
<tr>
<th><button id='toggle-sql-table'><i class='fa fa-eye'></i></button></th>
<th>N</th>
<th>Q</th>
<th>M</th>
<th>C</th>
<th>T</th>
</tr>";
foreach ($this->debug as $key => $array) {
++$key;
$return .= "<tr style='display:none'>";
$return .= "<td>$key</td>";
foreach ($array as $value) {
$return .= "<td>$value</td>";
}
$return .= "</tr>";
}
$total_time = array_sum($this->time_array);
$return .= "
<tr style='display:none'>
<th>#</th>
<th>Name</th>
<th>Query</th>
<th>Mode</th>
<th>Cache</th>
<th>$total_time</th>
</tr>
</table>";
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
// ======== Add cache table // ======== Add cache table
$cacheDebug = $memcached->toArray(); $cacheDebug = $memcached->toArray();
$return .= " $return .= "
<table style='margin-top: 50px;' class='table table-condensed table-striped'> <table style='margin-top: 50px;' class='table table-condensed table-striped'>
<tr> <tr>
<th><button id='toggle-cache-table'><i class='fa fa-eye'></i></button></th> <th><button id='toggle-cache-table'><i class='fa fa-eye'></i></button></th>
<th>Method</th> <th>Method</th>
<th>Time (s)</th> <th>Time (s)</th>
<th>Key</th> <th>Key</th>
<th>Result</th> <th>Result</th>
<th>Call Stack</th> <th>Call Stack</th>
</tr>"; </tr>";
$return .= " $return .= "
<tr style='display:none'> <tr style='display:none'>
@ -256,18 +288,18 @@ class SQL extends PDO {
$total_time = array_sum($this->time_array); $total_time = array_sum($this->time_array);
$return .= " $return .= "
<tr style='display:none'> <tr style='display:none'>
<th>#</th> <th>#</th>
<th>Method</th> <th>Method</th>
<th>Time (s)</th> <th>Time (s)</th>
<th>Key</th> <th>Key</th>
<th>Result</th> <th>Result</th>
<th>$cacheDebug[time]</th> <th>$cacheDebug[time]</th>
</tr> </tr>
</table>"; </table>";
} }
$return .= " $return .= "
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
$('#toggle-sql-table').click(function(ev){ $('#toggle-sql-table').click(function(ev){
@ -287,8 +319,8 @@ class SQL extends PDO {
}); });
</script>"; </script>";
return $return; return $return;
} }
} }
try { try {
@ -305,7 +337,7 @@ require_once ABSPATH . '/scripts/classes/cache.class.req.php';
if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) { if (defined('CAPTURE_CACHE_STATS') && CAPTURE_CACHE_STATS) {
$memcached = new Cache(); $memcached = new Cache();
} else { } else {
$memcached = new Memcached(); $memcached = new Synced_Memcached();
} }
$memcached->addServer(MEMCACHED_HOST, 11211); $memcached->addServer(MEMCACHED_HOST, 11211);
@ -314,7 +346,7 @@ require_once (ABSPATH . '/scripts/functions.req.php');
foreach (read_dir('scripts/classes') as $file) { foreach (read_dir('scripts/classes') as $file) {
$file = str_replace(['..', '/', '\\', '`', '´', '"', "'"], '', $file); $file = str_replace(['..', '/', '\\', '`', '´', '"', "'"], '', $file);
require_once (ABSPATH . "/scripts/classes/$file"); require_once (ABSPATH . "/scripts/classes/$file");
} //require every file in classes } //require every file in classes
require_once (ABSPATH . '/scripts/display.req.php'); require_once (ABSPATH . '/scripts/display.req.php');

View File

@ -0,0 +1,5 @@
<?php
if ($user->user_id){
print jquery_post("change_activation_email", 0, "check", "Confirm", "Confirming", "Your email has been updated.", "");
}
?>

View File

@ -397,6 +397,8 @@ $(".mass_edit_delete_button").click(function(event){
<?= jquery_get("manga_unlock", $id, '', "Unlock", "Unlocking", "You have unlocked this manga.", "location.reload();") ?> <?= jquery_get("manga_unlock", $id, '', "Unlock", "Unlocking", "You have unlocked this manga.", "location.reload();") ?>
<?= jquery_get("manga_regenerate_thumb", $id, '', "Regenerate thumb", "Regenerating thumb", "You have regenerated the thumbnail.", "location.reload();") ?>
<?php } ?> <?php } ?>
<?php if (validate_level($user, 'gmod')) { ?> <?php if (validate_level($user, 'gmod')) { ?>

View File

@ -1,7 +1,93 @@
$("#user_search_form").submit(function(event) { <?php
var email = encodeURIComponent($("#email").val()); switch ($mode) {
var username = encodeURIComponent($("#username").val()); case 'email_search':
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true); ?>
location.href = "/pr/?username="+username+"&email="+email; $("#user_search_form").submit(function(event) {
event.preventDefault(); var email = encodeURIComponent($("#email").val());
}); var username = encodeURIComponent($("#username").val());
$("#search_button").html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?> Searching...").attr("disabled", true);
location.href = "/pr/email_search?username="+username+"&email="+email;
event.preventDefault();
});
<?php
break;
case 'banners':
default:
?>
$("#banner_upload_form").submit(function(evt) {
evt.preventDefault();
$("#banner_upload_button").html("<span class='fas fa-spinner fa-pulse' aria-hidden='true' title=''></span> Uploading...").attr("disabled", true);
const success_msg = "<div class='alert alert-success text-center' role='alert'><strong>Success:</strong> Your banner has been uploaded.</div>";
const error_msg = "<div class='alert alert-warning text-center' role='alert'><strong>Warning:</strong> Something went wrong with your upload.</div>";
const form = this;
const formdata = new FormData(form);
$.ajax({
url: "/ajax/actions.ajax.php?function=banner_upload",
type: 'POST',
data: formdata,
cache: false,
contentType: false,
processData: false,
success: function (data) {
if (!data) {
$("#message_container").html(success_msg).show().delay(3000).fadeOut();
location.reload();
}
else {
$("#banner_upload_button").html("<?= display_fa_icon('upload') ?> Upload").attr("disabled", false);
$("#message_container").html(data).show().delay(5000).fadeOut();
}
},
error: function(err) {
console.error(err);
$("#banner_upload_button").html("<?= display_fa_icon('upload') ?> Upload").attr("disabled", false);
$("#message_container").html(error_msg).show().delay(5000).fadeOut();
}
});
});
$(".toggle_banner_edit_button, .cancel_banner_edit_button").click(function(evt) {
evt.preventDefault();
let id = $(this).attr("data-toggle");
$("#banner_edit_" + id).toggle();
$("#banner_" + id).toggle();
});
$(".banner_edit_form").submit(function(evt) {
evt.preventDefault();
const id = $(this).attr("data-banner-id");
const success_msg = "<div class='alert alert-success text-center' role='alert'><strong>Success:</strong> Your banner has been edited.</div>";
const error_msg = "<div class='alert alert-warning text-center' role='alert'><strong>Warning:</strong> Something went wrong with your edit.</div>";
const formData = new FormData($(this)[0]);
$("#banner_edit_button_"+id).html("<?= display_fa_icon('spinner', '', 'fa-pulse') ?>").attr("disabled", true);
$.ajax({
url: "/ajax/actions.ajax.php?function=banner_edit&banner_id=" + id,
type: 'POST',
data: formData,
cache: false,
contentType: false,
processData: false,
success: function(data) {
if (!data) {
$("#message_container").html(success_msg).show().delay(3000).fadeOut();
}
else {
$("#message_container").html(data).show().delay(10000).fadeOut();
}
$("#banner_edit_button_" + id).html("<?= display_fa_icon('pencil-alt', '', 'fa-fw') ?>").attr("disabled", false);
},
error: function(err) {
console.error(err);
$("#banner_edit_button_" + id).html("<?= display_fa_icon('pencil-alt', '', 'fa-fw') ?>").attr("disabled", false);
$("#message_container").html(error_msg).show().delay(10000).fadeOut();
}
});
});
<?php
break;
}
?>

View File

@ -1,16 +0,0 @@
/* closest */
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
var el = this;
do {
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,213 @@
import EventEmitter from 'wolfy87-eventemitter'
export default class ReaderPageModel extends EventEmitter {
constructor(number, chapterId, url, fallbackURL) {
super()
this._number = number
this._chapter = chapterId
this._state = ReaderPageModel.STATE_WAIT
this._error = null
this._url = url
this._fallbackURL = fallbackURL
this._image = new Image()
this.addImageListeners()
}
get number() { return this._number }
get chapter() { return this._chapter }
get image() { return this._image }
get waiting() { return this.state === ReaderPageModel.STATE_WAIT }
get loading() { return this.state === ReaderPageModel.STATE_LOADING }
get loaded() { return this.state === ReaderPageModel.STATE_LOADED }
get hasError() { return this.state === ReaderPageModel.STATE_ERROR }
get isDone() { return this.loaded || this.hasError }
get error() { return this._error }
get state() { return this._state }
set state(v) {
this._state = v
this.trigger('statechange', [this])
}
get stateName() {
switch (this.state) {
case 0: return 'wait'
case 1: return 'loading'
case 2: return 'loaded'
case 3: return 'error'
}
}
load(breakCache = false) {
return new Promise((resolve, reject) => {
if (!breakCache && this.isDone) {
return resolve(this)
} else {
if (!this.loading) {
this._error = null
this.unload()
loadImage(this._url, this._fallbackURL).then(url => {
this._image.src = url
}).catch(e => {
this._error = e
this.state = ReaderPageModel.STATE_ERROR
reject(this)
})
this.state = ReaderPageModel.STATE_LOADING
}
this.once('statechange', () => {
switch (this.state) {
case ReaderPageModel.STATE_LOADED: return resolve(this)
case ReaderPageModel.STATE_ERROR: return reject(this)
}
})
}
})
}
unload() {
try {
if (this._image.src && this._image.src.startsWith('blob:')) {
URL.revokeObjectURL(this._image.src)
this._image.src = ''
}
} catch () {}
}
addImageListeners() {
const _errorHandler = () => {
this._error = new Error(`Image #${this.number} failed to load.`)
this.state = ReaderPageModel.STATE_ERROR
this.unload()
}
const _loadHandler = () => {
this.state = ReaderPageModel.STATE_LOADED
try { this._image.decode() } catch (e) { }
}
this._image.addEventListener('error', _errorHandler)
this._image.addEventListener('load', _loadHandler)
}
reload(breakCache = false) {
return this.load(this.hasError || breakCache)
}
static get STATE_WAIT() { return 0 }
static get STATE_LOADING() { return 1 }
static get STATE_LOADED() { return 2 }
static get STATE_ERROR() { return 3 }
}
async function loadImage(primaryURL, fallbackURL) {
try {
await caches.delete('mangadex_images')
} catch () {}
// Otherwise fetch the image and time how long it takes
let resp, timing, body
if (!fallbackURL || primaryURL === fallbackURL || !window.AbortController) {
// If we only have one URL, load it normally
;[resp, timing, body] = await fetchWithTiming(primaryURL, null)
} else {
// Otherwise things get weird. We want to race the requests
// but also get a working response if possible.
const primaryA = new AbortController()
const fallbackA = new AbortController()
// Make a promise for each URL, starting the fallback 5s after the primary
const primaryP = fetchWithTiming(primaryURL, primaryA.signal).catch(e => {
return [new Response(null, { status: 599 }), 0, 0]
})
const fallbackP = sleep(5000, fallbackA.signal)
.then(() => {
return fetchWithTiming(fallbackURL, fallbackA.signal)
})
.catch(e => {
return [new Response(null, { status: 599 }), 0, 0]
})
// Get the first response
;[resp, timing, body] = await Promise.race([primaryP, fallbackP])
// If the first response failed, wait for the second
if (!resp.ok) {
const results = await Promise.all([primaryP, fallbackP])
// Find the first good result, if it exists
;[resp, timing, body] = results.find(([r]) => r.ok) || results[0]
}
// Cancel the other request
// We need seperate AbortControllers & abort calls since abort also cancels the *response*
// normalize the URLs so we can actually compare
if (new URL(resp.url).href === new URL(primaryURL).href) {
fallbackA.abort()
} else {
primaryA.abort()
}
}
// Async report to MD@H server how it went, if applicable
if (primaryURL && /mangadex\.network/.test(primaryURL)) {
const success = (new URL(resp.url).href === new URL(primaryURL).href) && resp.ok
fetch('https://api.mangadex.network/report', {
method: 'post',
mode: 'cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: primaryURL,
success: success,
bytes: success ? body.size : 0,
duration: success ? timing : 0,
cached: success && resp.headers.get('X-Cache') === 'HIT',
}),
keepalive: true, // Keep sending the report even if the user leaves the page as we send it
})
.then(res => {
if (!res.ok) throw res
})
.catch(e => null)
}
return URL.createObjectURL(body)
}
function sleep(ms, signal) {
return new Promise((resolve) => {
if (signal.aborted) return resolve()
const t = setTimeout(resolve, ms)
signal.addEventListener('abort', () => {
clearTimeout(t)
resolve()
})
})
}
async function fetchWithTiming(url, signal) {
let resp, body
const networkImage = /mangadex\.network/.test(url)
const start = 'performance' in self ? performance.now() : +new Date()
const hashM = /\/(?:data)\/.*-([0-9a-f]{64})\.[a-z]{3,4}$/.exec(url)
resp = await fetch(url, {
mode: 'cors',
cache: networkImage ? 'no-store' : 'default',
referrer: 'no-referrer',
redirect: 'error', // Cross-origin redirects strip required headers, so explicitly error instead
integrity: hashM ? `sha256-${hex2b64(hashM[1])}` : undefined, // if the image has a hash in it, check that it matches what we got
signal, // Cancel the fetch if we already got a response
})
body = await resp.blob()
const end = 'performance' in self ? performance.now() : +new Date()
return [resp, end - start, body]
}
function hex2b64(s) {
let b = ''
const n = Math.ceil(s.length / 2)
for (let i = 0, o = 0; i < n; i++, o += 2) {
b += String.fromCharCode(parseInt(s.substr(o, 2), 16))
}
return btoa(b)
}

View File

@ -0,0 +1,61 @@
import { StoreCookie, StoreLocalStorage } from './Store'
const SettingStore = Modernizr.localstorage ? StoreLocalStorage : StoreCookie
export default class ReaderSetting {
constructor(name, def, test, parser) {
this._name = name
this._default = def
this._value = def
this.test = test || ReaderSetting.getTest(def)
this.parser = parser || ReaderSetting.getParser(def)
}
get name() { return this._name }
get default() { return this._default }
get value() { return this._value }
set value(val) {
const parsedVal = this.parser(val)
if (this.test(parsedVal)) {
this._value = parsedVal
}
}
load() {
this.value = SettingStore.load(this.name)
}
save(val) {
this.value = val
if (this.value === this.default) {
SettingStore.clear(this.name)
} else {
SettingStore.save(this.name, this.value)
}
}
clear() {
SettingStore.clear(this.name)
}
static getTest(val) {
switch (typeof val) {
case 'number': return (val) => !isNaN(val)
case 'string': return (val) => typeof val === 'string'
default: return (val) => true
}
}
static getParser(val) {
switch (typeof val) {
case 'number': return (val) => parseFloat(val)
default: return (val) => val
}
}
static clearAllExcept(retainedKeys) {
SettingStore.clearAllExcept(retainedKeys)
}
}

54
scripts/reader/Store.js Normal file
View File

@ -0,0 +1,54 @@
import Cookies from 'js-cookie'
class Store {
static save(key, val) { }
static load(key) { }
static clear(key) { }
}
export class StoreLocalStorage extends Store {
static save(key, val) {
localStorage.setItem(`reader.${key}`, val)
}
static load(key) {
return localStorage.getItem(`reader.${key}`)
}
static clear(key) {
localStorage.removeItem(`reader.${key}`)
}
static clearAllExcept(retainedKeys) {
retainedKeys = retainedKeys.map(key => `reader.${key}`)
for (let [key, value] of Object.entries(localStorage)) {
if (key.startsWith('reader.') && retainedKeys.indexOf(key) === -1) {
localStorage.removeItem(key)
}
}
}
}
export class StoreCookie extends Store {
static get cookies() {
return document.cookie.split(';').reduce((a, c) => {
const [k, v] = c.split('=')
a[k.trim()] = v.trim()
return a
}, {})
}
static save(key, val) {
Cookies.set(`r-${key}`, val)
}
static load(key) {
return Coookies.get(`r-${key}`)
}
static clear(key) {
Coookies.remove(`r-${key}`)
}
static clearAllExcept(retainedKeys) {
retainedKeys = retainedKeys.map(key => `r-${key}`)
for (let [key, value] of Object.entries(Cookies.get())) {
if (key.startsWith('reader.') && retainedKeys.indexOf(key) === -1) {
Coookies.remove(key)
}
}
}
}

418
scripts/reader/api.js Normal file
View File

@ -0,0 +1,418 @@
import Utils from './utils.js'
import natsort from 'natsort'
export default class Resource {
get resourceType() { return 'resource' }
get resourceFormat() { return 'json' }
constructor(data = {}) {
this.initialize(data)
}
initialize(data = {}) {
this._data = data
}
get status() { return this._data.status }
load(opts = {}, force = false) {
const id = opts.id != null ? opts.id : ''
const type = this.resourceType
return new Promise((resolve, reject) => {
opts.type = type
if (!(type in Resource.cache)) {
Resource.cache[type] = {}
}
if (!force && id in Resource.cache[type]) {
return resolve(Resource.cache[type][id])
}
const baseURL = opts.baseURL || window.location
delete opts.baseURL
const url = new URL(this.constructor.API_URL(), baseURL)
for (let key in opts) {
url.searchParams.append(key, opts[key])
}
return resolve(fetch(url, {
credentials: 'same-origin',
}).catch(err => {
console.error(err)
this.response = err
this._data.message = err.message
throw err
}).then(res => {
this.response = res
if (!res.ok) {
console.error('Fetch not ok:', type, id, res)
return { id }
}
if (type === 'follows') {
return res.text()
} else {
return res.json().catch(err => {
console.error('JSON parsing error:', err)
return { id }
})
}
}).then(json => {
Resource.cache[type][id] = this
this.initialize(json)
if (!this.response.ok) {
console.error('Response status:', this.response.status, this.response.statusText)
console.error('Resource status:', json.status)
}
return this
}))
}).catch(err => {
console.error('Error:', err)
throw err
})
}
static create(opts, force) {
const resource = new this()
return resource.load(opts, force)
}
}
Resource.cache = {}
var xx = 0
export class Manga extends Resource {
get resourceType() {
return xx++ ? 'manga' : 'mangaa'
}
// static API_URL(id = '') { return `/api/manga/${id}` }
static API_URL() { return `/api/` }
initialize(data) {
super.initialize(data.manga)
this.chapters = Object.entries(data.chapter || {}).map(([id, ch]) => { ch.id = parseInt(id); return ch })
this.chapterList = []
}
get id() { return this._data.id }
get title() { return this._data.title || '' }
get langCode() { return this._data.lang_flag }
get langName() { return this._data.lang_name }
get lastChapter() { return this._data.last_chapter }
get isLongStrip() { return this._data.genres && this._data.genres.includes(36) }
get isDoujinshi() { return this._data.genres && this._data.genres.includes(7) }
get isHentai() { return !!this._data.hentai }
get url() {
const title = this.title.toLowerCase().replace(/&[0-9a-z]+;/gi, '').replace(/[^0-9a-z]/gi, ' ').split(' ').filter(t => t).join('-')
return `/title/${this.id}/${title}`
}
get coverUrl() { return `/images/manga/${this.id}.jpg` }
get coverThumbUrl() { return `/images/manga/${this.id}.thumb.jpg` }
getChapterData(id) {
return this.chapters.find(c => c.id === id)
}
getChapterTitle(id, noTitle = false) {
const ch = this.getChapterData(id)
if (!ch) {
return ''
} else {
let title = ''
if (ch.volume)
title += `Vol. ${ch.volume} `
if (ch.chapter)
title += `Ch. ${ch.chapter} `
if (ch.title && !noTitle)
title += `${ch.title}`
if (!title)
title = 'Oneshot'
return title.trim()
}
}
getChapterName(id) {
const ch = this.getChapterData(id)
if (!ch) {
return ''
} else {
if (ch.title)
return ch.title
if (ch.volume && ch.chapter)
return `Vol. ${ch.volume} Ch. ${ch.chapter}`
if (ch.chapter)
return `Ch. ${ch.chapter}`
if (ch.volume)
return `Vol. ${ch.volume}`
return 'Oneshot'
}
}
makeChapterList(lang, [g1 = 0, g2 = 0, g3 = 0]) {
this.chapterList = []
const sameLang = this.chapters.filter(c => c.lang_code === lang)
Manga.sortChapters(sameLang)
let best = null
for (let ch of sameLang) {
if (!best) {
best = ch
} else {
if (!ch.chapter && (!ch.volume || ch.volume === "0") || (best.chapter !== ch.chapter || best.volume !== ch.volume)) {
this.chapterList.push(best)
best = ch
} else if (ch.group_id === g1 && ch.group_id_2 === g2 && ch.group_id_3 === g3) {
best = ch
}
}
}
if (best) {
this.chapterList.push(best)
}
return this.chapterList
}
getAltChapters(id) {
const cur = this.getChapterData(id)
if (!cur) {
return []
} else {
const isNonNumbered = (cur.volume === "" || cur.volume === "0") && cur.chapter === ""
return this.chapters
.filter(c =>
c.lang_code === cur.lang_code
&& c.volume === cur.volume && c.chapter === cur.chapter
&& (!isNonNumbered || cur.title === c.title)
).map(c => new Chapter(c))
}
}
getPrevChapterId(id) {
const index = this.chapterList.findIndex(c => c.id === id)
if (index <= 0) {
return -1
} else {
return this.chapterList[index - 1].id
}
}
getNextChapterId(id) {
const index = this.chapterList.findIndex(c => c.id === id)
if (index === -1 || index === this.chapterList.length - 1) {
return 0
} else {
return this.chapterList[index + 1].id
}
}
areChaptersSequential(id1, id2) {
const c1 = this.getChapterData(id1)
const c2 = this.getChapterData(id2)
if (!c1 || !c2) {
return true
}
const c1Chapter = parseFloat(c1.chapter)
const c2Chapter = parseFloat(c2.chapter)
const c1Volume = parseFloat(c1.volume)
const c2Volume = parseFloat(c2.volume)
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
return true
} else if (c1Chapter === c2Chapter && c1Volume === c2Volume) {
return true
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
return true
} else if ((c1Chapter <= 1 && Math.floor(c1Volume - c2Volume) <= 1) || (c2Chapter <= 1 && Math.floor(c2Volume - c1Volume) <= 1)) {
return true
} else {
return false
}
}
/*areChaptersSequential(c1Chapter, c1Volume, c2Chapter, c2Volume) {
c1Chapter = parseFloat(c1Chapter)
c2Chapter = parseFloat(c2Chapter)
c1Volume = parseFloat(c1Volume)
c2Volume = parseFloat(c2Volume)
if (isNaN(c1Chapter) || isNaN(c2Chapter)) {
return true
} else if (Math.abs(c1Chapter - c2Chapter).toFixed(1) <= 1.1) {
return true
} else if ((c1Chapter === 1 || c2Chapter === 1) && Math.abs(c1Volume - c2Volume) <= 1) {
return true
}
return false
}*/
static sortChapters(chapters) {
const sorter = natsort({ desc: false, insensitive: true })
// sort by volume desc, so that vol null > vol number where ch are equal
Utils.stableSort(chapters, (a, b) => sorter(b.volume, a.volume))
// sort by first group
Utils.stableSort(chapters, (a, b) => sorter(a.group_id, b.group_id))
// sort by chapter number
Utils.stableSort(chapters, (a, b) => sorter(a.chapter, b.chapter))
// add ghost prev vol numbers
let pv = '0'
chapters.forEach(c => {
c.__prev_vol = pv
if (c.volume) {
pv = c.volume
}
})
// sort by vol or prev vol
Utils.stableSort(chapters, (a, b) => sorter(a.volume || a.__prev_vol, b.volume || b.__prev_vol))
// remove ghost vols
chapters.forEach(c => { delete c.__prev_vol })
}
static create(opts, force) {
return super.create(opts, force).then(manga => {
manga._data.id = opts.id
return manga
})
}
}
export class Chapter extends Resource {
get resourceType() { return 'chapter' }
// static API_URL(id = '') { return `/api/chapter/${id}` }
static API_URL() { return `/api/` }
get id() { return this._data.id }
get mangaId() { return this._data.manga_id }
get title() { return this._data.title }
get chapter() { return this._data.chapter }
get volume() { return this._data.volume }
get comments() { return this._data.comments }
get isLastChapter() { return this.manga && this.manga.lastChapter && this.manga.lastChapter !== "0" && this.manga.lastChapter === this.chapter }
get langCode() { return this._data.lang_code }
get langName() { return this._data.lang_name }
get totalPages() { return this._data.page_array ? this._data.page_array.length : 0 }
get groupIds() { return [this._data.group_id, this._data.group_id_2, this._data.group_id_3].filter(n => n) }
get groupNames() { return [this._data.group_name, this._data.group_name_2, this._data.group_name_3].filter(n => n) }
get groupWebsite() { return this._data.group_website }
get timestamp() { return this._data.timestamp }
get prevChapterId() { return this.manga.getPrevChapterId(this.id) }
get nextChapterId() { return this.manga.getNextChapterId(this.id) }
get url() { return `/chapter/${this.id}` }
get externalUrl() { return this._data.external || '' }
get fullTitle() {
let title = ''
if (this.volume) title += `Vol. ${this.volume} `
if (this.chapter) title += `Ch. ${this.chapter} `
if (this.title) title += `${this.title}`
if (!title) title = 'Oneshot'
return title.trim()
}
get isMangaFailed() { try { return !this.manga.response.ok } catch (err) { return true } }
get isNotFound() { try { return this.response.status == 404 } catch (err) { return false } }
get isDelayed() { try { return this.response.status == 409 } catch (err) { return false } }
get isDeleted() { try { return this.response.status == 410 } catch (err) { return false } }
get isRestricted() { try { return this.response.status == 451 } catch (err) { return false } }
get isExternal() { return this._data.status === 'external' }
get message() { return this._data.message }
get isNetworkServer() {
if (!this._isNetworkServer) {
this._isNetworkServer = /mangadex\.network/.test(this._data.server || '')
}
return this._isNetworkServer
}
get pageArray() { return this._data.page_array || [] }
getPage(pg) {
return pg >= 1 && pg <= this.totalPages ? this._data.page_array[pg - 1] : ''
}
get pagesFullURL() { return this.pageArray.map((pg, i) => this.imageURL(i + 1)) }
imageURL(pg) {
return this._data.server + this._data.hash + '/' + this.getPage(pg)
}
makeMangaChapterList() {
this.manga.makeChapterList(this.langCode, this.groupIds)
}
loadManga(force) {
if (!this._data.manga_id) {
console.warn('No manga id for chapter', this.id)
return Promise.resolve(this)
}
return Manga.create({ id: this._data.manga_id }, force).then(manga => {
this.manga = manga
if (!manga.response.ok) {
return Promise.reject(this)
} else {
this.makeMangaChapterList()
return Promise.resolve(this)
}
})
}
static create(opts, force) {
return super.create(opts, force)
.catch(ch => ch.mangaId ? Promise.resolve(ch) : Promise.reject(ch))
.then(ch => {
if (ch._data.page_array) {
ch._data.page_array = ch._data.page_array.filter(p => !!p)
}
return Promise.resolve(ch)
})
.then(ch => ch.loadManga(force))
.catch(ch => ch.manga && !ch.isMangaFailed ? Promise.resolve(ch) : Promise.reject(ch))
}
}
export class Follows extends Resource {
get resourceType() { return 'follows' }
get resourceFormat() { return 'text' }
static API_URL() { return '/follows/' }
get unreadChapters() {
return this.chapters.filter(c => c.id && !c._data.read)
}
get unreadManga() {
return this.unreadChapters.reduce((acc, cur) => {
acc[cur.manga.id] = cur.manga
return acc
}, {})
}
static create(opts, force) {
return super.create(opts, force).then(follows => {
const rows = follows._data.match(/col-md-3 [\s\S]*?chapter-list-group/gim)
if (!rows) {
return []
}
const mangaCache = {}
let mangaTitle = ''
follows.chapters = rows.map(row => {
const none = ['', '']
mangaTitle = (row.match(/manga_title[\s\S]*?title='([\s\S]*?)'/) || none)[1].trim() || ''
if (mangaTitle) { console.log(mangaTitle) }
const mangaId = parseInt((row.match(/data-manga-id="(\d*?)"/) || none)[1])
if (!(mangaId in mangaCache)) {
mangaCache[mangaId] = new Manga()
mangaCache[mangaId].initialize({
manga: {
id: mangaId || 0,
title: mangaTitle,
}
})
}
const manga = mangaCache[mangaId]
const chapter = new Chapter()
chapter.initialize({
id: parseInt((row.match(/data-id="(\d*?)"/) || none)[1]) || null,
title: (row.match(/data-title="([\s\S]*?)"/) || none)[1],
chapter: parseFloat((row.match(/data-chapter="([\d\.]*?)"/) || none)[1]) || null,
volume: parseFloat((row.match(/data-volume="([\d\.]*?)"/) || none)[1]) || null,
timestamp: parseInt((row.match(/data-timestamp="(.*?)"/) || none)[1]) * 1000 || null,
lang_code: (row.match(/flag-(..)/) || none)[1],
read: /chapter_mark_unread_button/.test(row),
})
chapter.manga = manga
manga.chapters.push(chapter)
return chapter
})
return follows
})
}
}
// export default { Manga, Chapter, Follows }

6
scripts/reader/index.js Normal file
View File

@ -0,0 +1,6 @@
import Reader from './reader-controller.js'
const reader = new Reader()
reader.initialize()
window.reader = reader

View File

@ -0,0 +1,138 @@
import ReaderView from './reader-view.js'
export default class KeyboardShortcuts {
static registerDefaults() {
const defaultScroll = 50
// kbd shortcuts
// ^ only when shift is pressed
// ! only when shift is not pressed
this.register('turnPageLeft', ['arrowleft', 'left', 'a'], function (evt, view) {
if (ReaderView.isScrolledToLeft) {
view.turnPageLeft(evt.shiftKey ? 1 : undefined)
}
})
this.register('turnPageRight', ['arrowright', 'right', 'd'], function (evt, view) {
if (ReaderView.isScrolledToRight) {
view.turnPageRight(evt.shiftKey ? 1 : undefined)
}
})
this.register('turnPageUp', ['arrowup', 'up', 'w'], function (evt, view) {
if (view.model.settings.pageWheelTurn == 1 && ReaderView.isScrolledToTop) {
view.turnPageBackward(evt.shiftKey ? 1 : undefined)
}
})
this.register('turnPageDown', ['arrowdown', 'down', 's'], function (evt, view) {
if (view.model.settings.pageWheelTurn == 1 && ReaderView.isScrolledToBottom) {
view.turnPageForward(evt.shiftKey ? 1 : undefined)
}
})
this.register('scrollLeft', ['arrowleft', 'left', 'a'], function (evt, view) {
if (view.model.settings.scrollingMethod == 1) {
ReaderView.scroll(-Math.floor(view.el.clientWidth * 0.9), 0, 'smooth')
} else if (view.model.settings.scrollingMethod == 0) {
const key = evt.key.toLowerCase()
if (key !== 'arrowleft' && key !== 'left') {
ReaderView.scroll(-defaultScroll, 0)
}
}
})
this.register('scrollRight', ['arrowright', 'right', 'd'], function (evt, view) {
if (view.model.settings.scrollingMethod == 1) {
ReaderView.scroll(Math.floor(view.el.clientWidth * 0.9), 0, 'smooth')
} else if (view.model.settings.scrollingMethod == 0) {
const key = evt.key.toLowerCase()
if (key !== 'arrowright' && key !== 'right') {
ReaderView.scroll(defaultScroll, 0)
}
}
})
this.register('scrollUp', ['arrowup', 'up', 'w'], function (evt, view) {
if (view.model.settings.scrollingMethod == 1) {
ReaderView.scroll(0, -Math.floor(view.el.clientHeight * 0.9), 'smooth')
} else if (view.model.settings.scrollingMethod == 0) {
const key = evt.key.toLowerCase()
if (key !== 'arrowup' && key !== 'up') {
ReaderView.scroll(0, -defaultScroll)
}
}
})
this.register('scrollDown', ['arrowdown', 'down', 's'], function (evt, view) {
if (view.model.settings.scrollingMethod == 1) {
ReaderView.scroll(0, Math.floor(view.el.clientHeight * 0.9), 'smooth')
} else if (view.model.settings.scrollingMethod == 0) {
const key = evt.key.toLowerCase()
if (key !== 'arrowdown' && key !== 'down') {
ReaderView.scroll(0, defaultScroll)
}
}
})
this.register('turnChapterLeft', ['^q'], function (evt, view) {
view.moveToChapter(view.model.isDirectionLTR ? view.model.chapter.prevChapterId : view.model.chapter.nextChapterId, 1)
})
this.register('turnChapterRight', ['^e'], function (evt, view) {
view.moveToChapter(view.model.isDirectionRTL ? view.model.chapter.prevChapterId : view.model.chapter.nextChapterId, 1)
})
this.register('toggleDisplayFit', ['f'], function (evt, view) {
view.model.saveSetting('displayFit', view.model.displayFit % 2 + (evt.shiftKey ? 3 : 1))
})
this.register('toggleRenderingMode', ['g'], function (evt, view) {
view.model.saveSetting('renderingMode', ((view.model.renderingMode) + (evt.shiftKey ? -1 : 1)) % 3 || 3)
})
this.register('toggleDirection', ['h'], function (evt, view) {
view.model.saveSetting('direction', view.model.direction % 2 + 1)
})
this.register('toggleHeader', ['!r'], function (evt, view) {
view.model.saveSetting('hideHeader', view.model.settings.hideHeader ? 0 : 1)
})
this.register('toggleSidebar', ['!t'], function (evt, view) {
view.model.saveSetting('hideSidebar', view.model.settings.hideSidebar ? 0 : 1)
})
this.register('togglePagebar', ['!y'], function (evt, view) {
view.model.saveSetting('hidePagebar', view.model.settings.hidePagebar ? 0 : 1)
})
this.register('toggleAllBars', ['^r', '^t', '^y'], function (evt, view) {
let any = view.model.settings.hideSidebar || view.model.settings.hideHeader || view.model.settings.hidePagebar
view.model.saveSetting('hideSidebar', any ? 0 : 1)
view.model.saveSetting('hideHeader', any ? 0 : 1)
view.model.saveSetting('hidePagebar', any ? 0 : 1)
})
this.register('exitToManga', ['^m'], function (evt, view) {
view.exitToURL(view.model.manga.url)
})
this.register('exitToComments', ['^k'], function (evt, view) {
view.exitToURL(`${view.pageURL(view.model.chapter.id)}/comments`)
})
}
static register(action, keys, handler) {
this._kbdInput = this._kbdInput || {}
this._kbdHandlers = this._kbdHandlers || {}
this._kbdHandlers[action] = handler
for (let key of keys) {
this._kbdInput[key] = this._kbdInput[key] || []
this._kbdInput[key].push(action)
}
}
static fire(key, evt, view) {
if (key in this._kbdInput) {
for (let action of this._kbdInput[key]) {
if (action in this._kbdHandlers) {
this._kbdHandlers[action](evt, view)
}
}
}
}
static keydownHandler(evt, view) {
if (!(evt.altKey || evt.ctrlKey || evt.metaKey || evt.key === 'OS')) {
const tag = (evt.target || evt.srcElement).tagName
const key = evt.key.toLowerCase()
if (!['INPUT','SELECT','TEXTAREA'].includes(tag)) {
evt.stopPropagation()
this.fire(key, evt, view)
this.fire(evt.shiftKey ? '^'+key : '!'+key, evt, view)
}
}
}
}

View File

@ -0,0 +1,89 @@
export default class ReaderComponent {
static create(type, props) {
const el = typeof type === 'string' ? document.createElement(type) : type
for (let i in props) {
el[i] = props[i]
}
return el
}
static empty(node) {
while (node && node.firstChild) {
node.removeChild(node.firstChild)
}
}
}
export class Option extends ReaderComponent {
static render(data) {
return this.create('option', {
value: data.value,
selected: data.selected || false,
textContent: data.text,
})
}
}
export class Flag extends ReaderComponent {
static render(data, el) {
const langCode = (data.language || '_unknown').replace(/\W/g, '')
el = this.create(el || 'span', {
title: langCode,
})
el.className = ''
el.classList.add('rounded', 'flag', `flag-${langCode}`.trim())
return el
}
}
export class Link extends ReaderComponent {
static render(data, el) {
return this.create(el || 'a', {
href: data.url,
title: data.title,
innerHTML: data.title,
})
}
}
export class ChapterDropdown extends ReaderComponent {
static render(model, el) {
this.empty(el)
for (let ch of model.manga.uniqueChapterList.slice().reverse()) {
el.appendChild(Option.render({
value: ch.id,
selected: ch.id === model.chapter.id,
text: model.settings.showDropdownTitles ? ch.fullTitle : ch.numberTitle,
}))
}
return el
}
}
export class PageDropdown extends ReaderComponent {
static render(model, el) {
this.empty(el)
for (let i = 1; i <= model.chapter.totalPages; ++i) {
el.appendChild(Option.render({
value: i,
selected: i === model.currentPage,
text: i
}))
}
return el
}
}
export class GroupItem extends ReaderComponent {
static render(chapter) {
const li = this.create('li')
const flag = li.appendChild(Flag.render(chapter))
flag.classList.add('mr-1')
const link = li.appendChild(this.create(!chapter.isCurrentChapter ? 'a' : 'strong', {
innerHTML: chapter.groups.map(g => g.name).join(' | '),
href: chapter.url
}))
link.dataset.action = "chapter"
link.dataset.chapter = chapter.id
return li
}
}

View File

@ -0,0 +1,55 @@
import ReaderModel from './reader-model'
import ReaderView from './reader-view'
export default class Reader {
constructor() {
this.model = new ReaderModel()
this.view = new ReaderView(this.model)
}
initialize() {
return new Promise((resolve, reject) => {
const meta = document.querySelector('meta[name="app"]')
this.model.appMeta = meta ? meta.dataset : {}
this.model.loadSettings()
for (let mode of ['renderingMode', 'displayFit', 'direction']) {
this.model[mode] = this.model.settings[mode]
}
if (typeof window === 'undefined') {
return reject()
}
return resolve()
}).then(() => {
this.view.initialize(document.querySelector('div[role="main"]'))
this.view.addListeners()
if (this.model.appMeta.page === 'recs') {
return this.model.moveToRecommendations()
.then(() => {
this.view.renderer.render()
})
}
let page = parseInt(this.model.appMeta.page) || 1
if (page === -1 && this.model.chapter) {
page = this.model.chapter.totalPages
}
return this.model.setChapter(parseInt(this.model.appMeta.chapterId), page)
.then((chapter) => {
if (this.model.appMeta.page === 'recs') {
this.view.moveToRecommendations()
} else if (!chapter.error) {
this.model.preload(page)
if (this.model.isStateReading) {
return this.view.moveToPage(page, false).then(() => {
this.view.replaceHistory()
this.view.updatePage()
})
}
} else {
this.view.replaceHistory(chapter.id, null)
}
})
}).catch((err) => {
console.error(err)
})
}
}

View File

@ -0,0 +1,445 @@
import EventEmitter from 'wolfy87-eventemitter'
import Chapter from './resource/Chapter'
import Manga from './resource/Manga'
import Follows from './resource/Follows'
import Utils from './utils.js'
import ReaderPageModel from './ReaderPageModel'
import ReaderSetting from './ReaderSetting'
export default class ReaderModel extends EventEmitter {
constructor() {
super()
this._state = ReaderModel.readerState.READING
this._isLoading = false
this._currentPage = 0
this._chapter = null
this._renderingMode = 0
this._displayFit = 0
this._direction = 0
this._appMeta = {}
this._settings = {}
this._settingsShortcut = {}
this._pageCache = new Map()
this._preloadSet = new Set()
}
get appMeta() { return this._appMeta }
set appMeta(val) {
this._appMeta = val
}
get isUserGuest() { return this.appMeta.guest !== '0' }
get isLoading() { return this._isLoading }
set isLoading(val) {
val = !!val
if (this._isLoading !== val && !this.exiting) {
this._isLoading = val
this.trigger('loadingchange', [val])
}
}
get currentPage() { return this._currentPage }
setCurrentPage(val) {
if (this._currentPage !== val && !isNaN(val) && !this.exiting) {
this._currentPage = val
this.trigger('currentpagechange', [val])
return true
}
}
get totalPages() { return this.chapter ? this.chapter.totalPages : 0 }
get state() { return this._state }
set state(val) {
if (this._state !== val && !this.exiting) {
this._state = val
this.trigger('statechange', [val])
}
}
get isStateReading() { return this._state === ReaderModel.readerState.READING }
get isStateRecommendations() { return this._state === ReaderModel.readerState.RECS }
get isStateGap() { return this._state === ReaderModel.readerState.GAP }
get exiting() { return this._state === ReaderModel.readerState.EXITING }
exitReader() {
this.state = ReaderModel.readerState.EXITING
// bfcache
window.onunload = () => { this.state = ReaderModel.readerState.READING }
}
get renderingMode() { return this._renderingMode }
set renderingMode(val) {
if (this._renderingMode !== val && !this.exiting) {
this._renderingMode = val
this.trigger('renderingmodechange', [val])
}
}
get displayFit() { return this._displayFit }
set displayFit(val) {
if (this._displayFit !== val && !this.exiting) {
this._displayFit = val
this.trigger('displayfitchange', [val])
}
}
get direction() { return this._direction }
set direction(val) {
if (this._direction !== val && !this.exiting) {
this._direction = val
this.trigger('directionchange', [val])
}
}
get isSinglePage() { return this.renderingMode === ReaderModel.renderingModeState.SINGLE }
get isDoublePage() { return this.renderingMode === ReaderModel.renderingModeState.DOUBLE }
get isLongStrip() { return this.renderingMode === ReaderModel.renderingModeState.LONG }
get isNoResize() { return this.displayFit === ReaderModel.displayFitState.NO_RESIZE }
get isFitHeight() { return this.displayFit === ReaderModel.displayFitState.FIT_HEIGHT }
get isFitWidth() { return this.displayFit === ReaderModel.displayFitState.FIT_WIDTH }
get isFitBoth() { return this.displayFit === ReaderModel.displayFitState.FIT_BOTH }
get isDirectionLTR() { return this.direction === ReaderModel.directionState.LTR }
get isDirectionRTL() { return this.direction === ReaderModel.directionState.RTL }
get settings() { return this._settingsShortcut }
get settingDefaults() {
return Array.from(Object.values(this._settings)).reduce((acc, setting) => {
acc[setting.name] = setting.default
return acc
}, {})
}
saveSetting(key, value) {
const setting = this._settings[key]
setting.save(value)
this._settingsShortcut[setting.name] = setting.value
if (['renderingMode', 'displayFit', 'direction'].includes(key)) {
this[key] = setting.value
}
this.trigger('settingchange', [setting.name, setting.value])
}
loadSettings() {
const defaults = [
new ReaderSetting('displayFit', ReaderModel.displayFitState.FIT_WIDTH),
new ReaderSetting('direction', ReaderModel.directionState.LTR),
new ReaderSetting('renderingMode', ReaderModel.renderingModeState.SINGLE),
new ReaderSetting('showAdvancedSettings', 0),
new ReaderSetting('scrollingMethod', 0),
new ReaderSetting('swipeDirection', 0),
new ReaderSetting('swipeSensitivity', 3),
new ReaderSetting('pageTapTurn', 1),
new ReaderSetting('pageTurnLongStrip', 1),
new ReaderSetting('pageWheelTurn', 0),
new ReaderSetting('showDropdownTitles', 1),
new ReaderSetting('tapTargetArea', 1),
new ReaderSetting('hideHeader', 0),
new ReaderSetting('hideSidebar', 0),
new ReaderSetting('hidePagebar', 0),
new ReaderSetting('collapserStyle', 0),
new ReaderSetting('hideCursor', 0),
new ReaderSetting('betaRecommendations', 0),
new ReaderSetting('imageServer', '0'),
new ReaderSetting('dataSaverV2', 0),
new ReaderSetting('gapWarning', 1),
new ReaderSetting('restrictChLang', 1, null, (val) => {
val = parseInt(val)
if (this.chapter && this.manga) {
this.manga.updateChapterList(this.chapter, val)
this.trigger('chapterlistchange', [])
}
return val
}),
new ReaderSetting('preloadPages', 10, () => true, (val) => {
if (isNaN(parseInt(val)))
return 10
const clamped = Utils.clamp(parseInt(val), 0, this.preloadMax)
return !isNaN(clamped) ? clamped : 0
}),
new ReaderSetting('containerWidth', null, (val) => {
return !val || !isNaN(parseInt(val))
}),
]
for (let setting of defaults) {
setting.load()
this._settings[setting.name] = setting
this._settingsShortcut[setting.name] = setting.value
this.trigger('settingchange', [setting.name, setting.value])
}
this.saveSetting('gapWarning', 1)
ReaderSetting.clearAllExcept(defaults.map(s => s.name))
}
get chapter() { return this._chapter }
get manga() { return this._chapter ? this._chapter.manga : null }
getChapterParams() {
return {
server: this.settings.imageServer !== '0' ? this.settings.imageServer : null,
saver: this.settings.dataSaverV2,
}
}
async setChapter(id, pg = 1) {
if (this.exiting || this.isLoading) {
throw new Error(`Trying to set chapter while ${this.exiting ? 'exiting' : 'loading'}`)
} else if (isNaN(id) || id <= 0) {
throw new Error("Trying to set invalid chapter: " + id)
}
try {
this.isLoading = true
let chapter = Chapter.getResource(id)
if (chapter && !chapter.isFullyLoaded) {
chapter = await Chapter.load(id, this.getChapterParams())
} else if (!chapter) {
chapter = await Chapter.loadWithManga(id, this.getChapterParams())
await Manga.loadChapterList(chapter.manga.id)
}
const oldManga = this.manga
this._chapter = chapter
this._currentPage = pg
this.isLoading = false
chapter.manga.updateChapterList(chapter, this.settings.restrictChLang)
this._createPageCache(chapter)
this.state = ReaderModel.readerState.READING
this.trigger('chapterchange', [chapter])
if (!oldManga || oldManga.id !== chapter.manga.id) {
this.trigger('mangachange', [chapter.manga])
}
if (chapter.error || chapter.isExternal) {
throw chapter
} else if (chapter.totalPages === 0) {
chapter.error = new Error("Chapter has no pages.")
throw chapter
}
if (chapter.isNetworkServer) {
this.preload(this.currentPage, Infinity)
}
return chapter
} catch (error) {
console.error(error)
this.isLoading = false
this.state = ReaderModel.readerState.ERROR
this.trigger('readererror', [error])
return error
}
}
async reloadChapterList() {
if (this.manga.isChapterListOutdated) {
this.isLoading = true
try {
await Manga.loadChapterList(this.manga.id)
} catch (error) { }
this.isLoading = false
}
}
_createPageCache(chapter) {
for (let [i, page] of this._pageCache) {
page.unload()
page.off()
}
this._pageCache.clear()
this._preloadSet.clear()
this._preloading = false
let pgNum = 1
for (let [url, fallbackURL] of chapter.getAllPageUrls()) {
const page = new ReaderPageModel(pgNum, chapter.id, url, fallbackURL)
this._pageCache.set(pgNum, page)
page.on('statechange', (page) => {
switch (page.state) {
case ReaderPageModel.STATE_LOADING: return this.trigger('pageloading', [page])
case ReaderPageModel.STATE_LOADED: return this.trigger('pageload', [page])
case ReaderPageModel.STATE_ERROR: return this.trigger('pageerror', [page])
}
})
++pgNum
}
}
_loadPage(pg, skipCache = false) {
const page = this._pageCache.get(pg)
if (!page) {
return Promise.reject(new Error(`Page ${pg} not in cache`))
} else {
return page.load(skipCache)
}
}
getPage(pg) {
return new Promise((resolve, reject) => {
if (this.chapter == null || this._pageCache.size === 0) {
return reject(new Error("Tried to get a page before chapter has loaded"))
} else if (isNaN(pg) || pg < 1 || pg > this.totalPages) {
return resolve(null)
// return reject(new Error("Page not a number or out of bounds"))
} else if (this._pageCache.get(pg).loaded) {
return resolve(this._pageCache.get(pg))
} else {
return resolve(this._loadPage(pg))
}
})
}
reloadErrorPages() {
Array.from(this._pageCache.values()).filter(i => i.hasError).forEach(i => i.reload())
}
getPageWithoutLoading(pg) {
if (!this._pageCache.has(pg)) {
//throw new Error(`No page ${pg} set.`)
return null
}
return this._pageCache.get(pg)
}
get currentPageObject() { return this.getPageWithoutLoading(this.currentPage) }
get isPageCacheEmpty() { return this._pageCache.size === 0 }
getAllPages() {
return Array.from(this._pageCache.values())
}
getLoadedPages() {
return this.getAllPages().filter(i => i.loaded || i.hasError)
}
_preloadNextInSet() {
if (this._preloadSet.size > 0) {
this._preloading = true
const pg = [...this._preloadSet][0]
this._preloadSet.delete(pg)
this._loadPage(pg)
.catch((page) => { /*console.warn(`Preload failed for page ${pg}`)*/ })
.then(() => this._preloadNextInSet())
} else {
this._preloading = false
}
}
_preloadArray(pages) {
if (pages.length > 0) {
pages
.filter(pg => !this.getPageWithoutLoading(pg).loaded)
.forEach(pg => this._preloadSet.add(pg))
if (!this._preloading) {
return this._preloadNextInSet()
}
}
}
get preloadMax() {
return this.isUserGuest ? PRELOAD_MAX_GUEST : PRELOAD_MAX_USER
}
// TODO: preload backwards iff [current, end] already loaded
preload(start = this.currentPage + 1, amount = this.settings.preloadPages) {
if (this.isPageCacheEmpty) {
return
}
if (amount == null) {
amount = 10
}
if (amount !== Infinity) {
amount = Utils.clamp(amount, 0, this.chapter.isNetworkServer ? this.totalPages : this.preloadMax)
}
start = Utils.clamp(start, 1, this.totalPages + 1)
const end = Utils.clamp(start + amount, 1, this.totalPages + 1)
return this._preloadArray(Utils.range(start, end))
}
preloadEverything() {
return this.preload(1, Infinity) // lol
}
moveToPage(pg) {
if (!this.chapter || isNaN(pg)) {
return Promise.resolve()
} else if (pg <= -1) {
return this.moveToPage(this.totalPages)
} else if (pg === 0) {
return Promise.reject({ chapter: this.chapter.prevChapterId, page: -1 })
// return this.moveToChapter(this.chapter.prevChapterId, -1)
} else if (pg > this.totalPages) {
return Promise.reject({ chapter: this.chapter.nextChapterId, page: 1 })
// return this.moveToChapter(this.chapter.nextChapterId, 1)
} else {
if (this.currentPage === pg) {
this.trigger('currentpagechange', [pg])
} else {
this.setCurrentPage(pg)
}
return Promise.resolve()
}
}
moveToChapter(id, pg = 1) {
if (id <= 0) {
return Promise.reject({ chapter: id })
} else {
return this.setChapter(id, pg).then(() => {
if (this.chapter && this.totalPages > 0 && this.isStateReading) {
return this.moveToPage(pg)
} else {
return Promise.resolve()
}
})
}
}
moveToRecommendations() {
this.isLoading = true
return Follows.load({}, false)
.then(recs => {
this.recommendations = recs
this.isLoading = false
this.state = ReaderModel.readerState.RECS
return Promise.resolve(recs)
})
.catch(err => {
this.recommendations = null
this.isLoading = false
this.state = ReaderModel.readerState.ERROR
this.trigger('readererror', [err])
return Promise.reject(err)
})
}
}
const PRELOAD_MAX_USER = 20
const PRELOAD_MAX_GUEST = 5
ReaderModel.renderingModeState = {
SINGLE: 1,
DOUBLE: 2,
LONG: 3,
ALERT: 4,
RECS: 5,
}
ReaderModel.directionState = {
LTR: 1,
RTL: 2,
}
ReaderModel.displayFitState = {
FIT_BOTH: 1,
FIT_WIDTH: 2,
FIT_HEIGHT: 3,
NO_RESIZE: 4,
}
ReaderModel.readerState = {
ERROR: 0,
READING: 1,
RECS: 2,
EXITING: 3,
GAP: 4,
}

File diff suppressed because it is too large Load Diff

652
scripts/reader/renderer.js Normal file
View File

@ -0,0 +1,652 @@
import { formatDistance } from 'date-fns'
import Utils from './utils'
import ReaderView from './reader-view'
import Chapter from './resource/Chapter'
import Manga from './resource/Manga'
export default class AbstractRenderer {
constructor(container, model, view) {
this.el = container
this.model = model
this.view = view
this._initialized = false
}
get chapter() { return this.model.chapter }
initialize() {
// console.log('initialize',this.name)
this._initialized = true
this.clearImageContainer()
this.renderedPages = 0
this._pageStateHandler = (page) => {
//console.info('pagestatehandler', this.name, page)
this.pageStateHandler(page)
}
this.model.on('pageloading', this._pageStateHandler)
this.model.on('pageload', this._pageStateHandler)
this.model.on('pageerror', this._pageStateHandler)
}
destroy() {
if (!this._initialized) {
return
}
// console.log('destroy',this.name)
this._initialized = false
this.clearImageContainer()
this.model.off('pageloading', this._pageStateHandler)
this.model.off('pageload', this._pageStateHandler)
this.model.off('pageerror', this._pageStateHandler)
}
reinitialize() {
if (this._initialized) {
this.destroy()
}
this.initialize()
}
createAndAppendWrapper(page) {
return this.el.appendChild(this.updateWrapper(this.createWrapper(), page))
}
createWrapper() {
const wrapper = document.createElement('div')
const classes = [
'reader-image-wrapper',
'col-auto',
'my-auto',
'justify-content-center',
'align-items-center',
'noselect', 'nodrag',
'row', 'no-gutters',
]
wrapper.classList.add(...classes)
wrapper.dataset.state = 0
return wrapper
}
updateWrapper(wrapper, page = {}) {
//console.log('update wrapper to', page.number, page)
if (page.state !== parseInt(wrapper.dataset.state)) {
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild)
}
switch (page.state) {
case 1: wrapper.appendChild(this.createPageLoading()); break;
case 2: wrapper.appendChild(this.createPageLoaded()); break;
case 3: wrapper.appendChild(this.createPageError()); break;
}
}
wrapper.style.order = page.number || 0
wrapper.dataset.page = page.number || 0
wrapper.dataset.state = page.state || 0
switch (page.state) {
case 1: wrapper.querySelector('.loading-page-number').textContent = page.number; break;
case 2: wrapper.firstChild.src = page.image.src; break;
case 3: wrapper.querySelector('.alert .message').textContent = page.error.message; break;
}
return wrapper
}
createPageLoading() {
const container = document.createElement('div')
container.classList.add('m-5', 'd-flex', 'align-items-center', 'justify-content-center')
container.style.color = '#fff'
container.style.textShadow = '0 0 7px rgba(0,0,0,0.5)'
const spinner = container.appendChild(document.createElement('span'))
spinner.classList.add('fas', 'fa-circle-notch', 'fa-spin', 'position-absolute')
spinner.style.opacity = '0.5'
spinner.style.fontSize = '7em'
const pgNum = container.appendChild(document.createElement('span'))
pgNum.classList.add('loading-page-number')
pgNum.style.fontSize = '2em'
return container
}
createPageLoaded() {
const container = document.createElement('img')
container.draggable = false
container.classList.add('noselect', 'nodrag', 'cursor-pointer')
return container
}
createPageError() {
const container = Alert.container('', 'danger')
const tapMsg = container.appendChild(document.createElement('div'))
tapMsg.innerHTML = "Tap to reload."
container.addEventListener('click', evt => {
evt.preventDefault()
evt.stopPropagation()
const page = this.model.getPageWithoutLoading(parseInt(container.parentElement.dataset.page))
page.reload(true).catch(console.error)
})
return container
}
createMangaError(chapter) {
const container = Alert.container('', 'danger')
const tapMsg = container.appendChild(document.createElement('div'))
tapMsg.innerHTML = "Tap to reload."
container.addEventListener('click', evt => {
evt.preventDefault()
evt.stopPropagation()
container.parentElement.removeChild(container)
this.model.isLoading = true
chapter.loadManga(true)
.then(chapter => {
this.model.isLoading = false
this.model.setChapter(chapter.id)
.then(() => { this.view.moveToPage(1, false) })
})
.catch(err => {
this.model.isLoading = false
this.model.trigger('readererror', [err])
})
})
return container
}
clearImageContainer() {
while (this.el && this.el.firstChild) {
this.el.removeChild(this.el.firstChild)
}
}
render() {
throw new Error("Not implemented")
}
pageStateHandler() {
throw new Error("Not implemented")
}
}
export class SinglePage extends AbstractRenderer {
get name() { return 'single-page' }
initialize() {
super.initialize()
this.renderedPages = 1
this.pageToRender = null
this.pageWrapper = this.createAndAppendWrapper()
}
pageStateHandler(page) {
if (this.pageToRender === page) {
this.updateWrapper(this.pageWrapper, page)
}
}
render(pg) {
const page = this.model.getPageWithoutLoading(pg)
this.pageToRender = page
this.updateWrapper(this.pageWrapper, page)
return page.load().catch(p => Promise.resolve(p))
}
}
export class DoublePage extends AbstractRenderer {
get name() { return 'double-page' }
get renderedPages() { return this.pagesToRender.length }
set renderedPages(v) { }
isPageTurnForwards() { return this.previousPage < this.model.currentPage }
isSinglePageBackwards() { return this.previousPage === this.model.currentPage + 1 }
isImageTooWide(img) {
return img && img.naturalWidth > img.naturalHeight && img.naturalWidth > this.el.offsetWidth / 2
}
initialize() {
super.initialize()
this.pageWrapperLoading = this.createAndAppendWrapper({ state: 1, number: '', })
this.previousPage = 0
this.pageWrappers = [this.createAndAppendWrapper(), this.createAndAppendWrapper()]
this.pagesToRender = []
this.setLoading(true)
}
pageStateHandler(page) {
if (this.pagesToRender.includes(page)) {
this.checkRender()
}
}
render(pg) {
this.pagesToRender = [pg, pg + 1]
.map(p => this.model.getPageWithoutLoading(p))
.filter(p => p)
this.checkRender()
return Promise.all(
this.pagesToRender.map(page =>
page.load().catch(p => Promise.resolve(p))
)
)
}
checkRender() {
const pagesDone = this.pagesToRender.every(p => p.isDone)
if (pagesDone) {
if (this.pagesToRender.length > 1 && this.pagesToRender.some(p => this.isImageTooWide(p.image))) {
if (this.isPageTurnForwards() || this.isSinglePageBackwards()) {
this.pagesToRender.pop()
} else {
this.pagesToRender.shift()
this.model.setCurrentPage(this.pagesToRender[0].number)
}
}
this.updateWrapper(this.pageWrappers[0], this.pagesToRender[0])
this.updateWrapper(this.pageWrappers[1], this.pagesToRender[1])
this.previousPage = this.model.currentPage
}
this.setLoading(!pagesDone)
}
setLoading(state) {
this.pageWrapperLoading.classList.toggle('d-none', !state)
for (let wrapper of this.pageWrappers) {
wrapper.classList.toggle('d-none', state)
}
}
}
export class LongStrip extends AbstractRenderer {
get name() { return 'long-strip' }
get renderedPages() { return this._renderedPageSet.length }
set renderedPages(v) { }
get lastRenderedPage() { return this._renderedPageSet[this._renderedPageSet.length - 1] }
initialize() {
super.initialize()
this._pageWrapperMap = new Map()
this._renderedPageSet = []
this._scrollY = -1
this.observer = new MutationObserver((mutationsList) => {
// this is horrible
if (this._scrollY === -1 && this.model.currentPage !== 1) {
this._scrollY = -2
requestAnimationFrame(() => {
this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
requestAnimationFrame(() => {
this._scrollY = window.pageYOffset || -1
// console.log('did it',this._scrollY)
if (this._scrollY !== -1) {
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
this.observer.disconnect()
}
})
})
//this.scrollToPage(this.model.currentPage)
}
})
this.observer.observe(this.el, { childList: true, subtree: true })
for (let page of this.model.getAllPages()) {
this._pageWrapperMap.set(page.number, this.createAndAppendWrapper(page))
if (page.isDone) {
this._renderedPageSet.push(page.number)
}
}
Utils.stableSort(this._renderedPageSet)
this.renderEndBlock()
this.render(this.model.currentPage)
this.addScrollHandler()
this._currentPageHandler = (pg) => {
this.render(pg + 1)
.then(() => {
this.render(pg - 1)
})
}
this.model.on('currentpagechange', this._currentPageHandler)
}
destroy() {
if (!this._initialized) {
return
}
super.destroy()
this.observer.disconnect()
//this.el.scrollIntoView(true)
this._pageWrapperMap.clear()
this.removeScrollHandler()
window.scrollTo(0, 0)
this.model.off('currentpagechange', this._currentPageHandler)
}
pageStateHandler(page) {
this.updateWrapper(this.getPageWrapper(page.number), page)
if (page.isDone || page.loading) {
if (!this.isRendered(page.number)) {
this._renderedPageSet.push(page.number)
Utils.stableSort(this._renderedPageSet)
}
if (this.isChapterFullyRendered) {
this.showEndBlock()
}
if (this._scrollY >= 0) {
this.updateCurrentPage()
} //else {
// this._scrollY = -2
// requestAnimationFrame(() => {
// this.getPageWrapper(this.model.currentPage).scrollIntoView(true)
// requestAnimationFrame(() => {
// ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
// requestAnimationFrame(() => {
// this._scrollY = window.pageYOffset || -1
// console.log('did it',this._scrollY)
// })
// })
// })
//this.scrollToPage(this.model.currentPage)
// }
}
}
getPageWrapper(pg) {
if (!this._pageWrapperMap.has(pg)) {
throw new Error("No wrapper for page ", pg)
}
return this._pageWrapperMap.get(pg)
}
isRendered(pg) {
return this._renderedPageSet.includes(pg)
}
get isChapterFullyRendered() {
return this.renderedPages === this.model.totalPages
}
render(pg) {
if (!this.isChapterFullyRendered && !this.isRendered(pg)) {
return this.model.getPage(pg)
.catch(p => Promise.resolve(p))
}
return Promise.resolve()
}
renderEndBlock() {
this._endBlock = this.createAndAppendWrapper({
number: this.model.totalPages + 1,
chapter: this.model.chapter.id,
})
this._endBlock.textContent = 'End of chapter / Go to next'
this._endBlock.classList.add('reader-image-block', 'py-3', 'd-none')
this._endBlock.addEventListener('click', (evt) => {
evt.stopPropagation()
this.view.moveToChapter(this.model.chapter.nextChapterId)
}, { once: true })
if (this.isChapterFullyRendered) {
this.showEndBlock()
}
}
showEndBlock() {
this._endBlock.classList.remove('d-none')
}
updateCurrentPage() {
if (this.renderedPages > 0 && !this._updating) {
this._updating = true
if (ReaderView.isScrolledToTop) {
this.model.setCurrentPage(this._renderedPageSet[0])
this.view.replaceHistory()
} else if (ReaderView.isScrolledToBottom) {
this.model.setCurrentPage(this.lastRenderedPage)
this.view.replaceHistory()
} else {
const scrollY = Math.floor(window.pageYOffset)
for (let i = this._renderedPageSet.length - 1; i >= 0; --i) {
const pg = this._renderedPageSet[i]
const wrapper = this.getPageWrapper(pg)
if (scrollY >= wrapper.offsetTop) {
if (this.model.setCurrentPage(pg)) {
this.view.replaceHistory()
}
break
}
}
}
this._updating = false
}
}
scrollToPage(pg) {
// requestAnimationFrame(() => {
const wrapper = this.getPageWrapper(pg)
if (this.isRendered(pg) && wrapper) {
// console.log('scrolling to', pg, wrapper.offsetTop + 1)
//window.scrollTo(window.pageXOffset, wrapper.offsetTop + 1)
wrapper.scrollIntoView(true)
if (!ReaderView.isScrolledToBottom) {
ReaderView.scroll(0, -document.querySelector('nav.navbar').offsetHeight + 1)
}
// requestAnimationFrame(() => {
// })
}
// })
}
addScrollHandler() {
if (!this._scrollHandler) {
const update = () => {
if (this.model.chapter) {
this.updateCurrentPage()
}
}
if (Modernizr.requestanimationframe) {
let wait = false
this._scrollHandler = () => {
if (!wait) {
wait = true
requestAnimationFrame(() => {
update()
wait = false
})
}
}
} else {
this._scrollHandler = () => {
update()
}
}
window.addEventListener('scroll', this._scrollHandler)
}
}
removeScrollHandler() {
if (this._scrollHandler) {
window.removeEventListener('scroll', this._scrollHandler)
this._scrollHandler = null
}
}
}
export class Alert extends AbstractRenderer {
get name() { return 'alert' }
pageStateHandler() { }
renderChapterButtons(data) {
const chBtnContainer = this.el.appendChild(document.createElement('div'))
chBtnContainer.classList.add('row', 'm-auto', 'justify-content-center', 'directional')
const buttons = [
{ text: 'Previous chapter', id: data.prevChapterId, order: 1 },
{ text: 'Next chapter', id: data.nextChapterId, order: 2 },
]
const classes = ['col-auto', 'hover', 'text-dark']
for (let btn of buttons) {
const link = chBtnContainer.appendChild(Alert.container(btn.text, 'dark', 'a'))
link.setAttribute('href', this.view.pageURL(btn.id))
link.dataset.action = 'chapter'
link.dataset.chapter = btn.id
link.classList.add(...classes)
link.classList.replace('m-auto', 'm-1')
link.style.order = btn.order
}
}
render(data) {
this.clearImageContainer()
if (typeof data !== 'object') {
return Promise.reject({ message: "Data is not an object", data: data, revert: true })
}
if (data.isExternal) {
this.el.appendChild(Alert.container(`This chapter can be read for free on the official publisher's website.<br>Feel free to write your comments about it here on MangaDex!`, 'info'))
const link = Alert.container(`${Alert.icon('external-link-alt', 'Website')} <strong>Read the chapter</strong>`, 'success', 'a', false)
link.target = '_blank'
link.rel = 'noopener noreferrer'
link.href = data.pages
this.el.appendChild(link)
this.renderChapterButtons(data)
} else if (data.isDelayed) {
const now = new Date()
const release = new Date(data.timestamp * 1000)
const relativeDate = release > now ? formatDistance(release, now, { addSuffix: true }) : 'within a few minutes'
this.el.appendChild(Alert.container(`Due to the group's delay policy, this chapter will be available ${relativeDate}.`, 'danger'))
this.el.appendChild(Alert.container(`You might be able to read it on the group's <a target='_blank' rel='noopener noreferrer' href='${data.groupWebsite}'>${Alert.icon('external-link-alt', 'Website')} <strong>website</strong></a>.`, 'info'))
this.renderChapterButtons(data)
} else if (data.isSpoilerNet) {
const alert = document.createElement('div')
alert.classList.add('alert', `alert-warning`, 'text-center', 'm-auto')
alert.attributes.role = 'alert'
alert.innerHTML = `<h3>${Alert.icon('warning')} Spoiler Warning</h3><div class="my-3"><div>There seems to be a gap between chapters (${Chapter.getResource(data.prevChapterId).fullTitle} &rarr; ${Chapter.getResource(data.chapterId).fullTitle}).</div><div>This may be an attempt to troll you into reading a chapter early.</div>`
const button = document.createElement('button')
button.classList.add('btn', 'btn-secondary')
button.type = 'button'
button.textContent = "I understand, I'm fine with spoilers!"
button.addEventListener('click', (evt) => {
evt.stopPropagation()
this.view.moveToChapter(data.chapterId, 1, true, true)
})
alert.appendChild(button)
this.el.appendChild(alert)
} else if (data.isNotFound) {
this.el.appendChild(Alert.container(`Data not found${data.message ? ': ' + data.message : '.'}`, 'danger'))
} else if (data.isDeleted) {
this.el.appendChild(Alert.container(`This chapter has been deleted.`, 'danger'))
} else if (data.isRestricted) {
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
} else if (data.isMangaFailed) {
const alert = this.createMangaError(data)
alert.querySelector('.message').textContent = "The manga data failed to load."
alert.draggable = false
alert.classList.add('noselect', 'nodrag', 'cursor-pointer')
this.el.appendChild(alert)
} else if (data.status === 'unavailable') {
this.el.appendChild(Alert.container(`This chapter is unavailable.`, 'danger'))
} else if (data != null) {
const isError = data instanceof Error || data.error instanceof Error
const type = data.type || isError ? 'danger' : undefined
const msg = data.message || data.error && data.error.message || data.error && data.error.status || data
this.el.appendChild(Alert.container(msg, type))
// if (isError) {
// this.el.appendChild(Alert.container('dark', data.stack || data.error.stack))
// }
}
return Promise.resolve()
}
static icon(type, title) {
type = Alert.iconTypes[type] || type
return `<span class='fas fa-${type} fa-fw' aria-hidden='true'${title ? ` title=${title}` : ''}></span> `
}
static get iconTypes() {
return {
success: 'check-circle',
danger: 'times',
info: 'info',
warning: 'exclamation-triangle',
}
}
static container(message = '', type = 'dark', element = 'div', icon = true) {
const div = document.createElement(element)
div.classList.add('alert', `alert-${type}`, 'text-center', 'm-auto')
div.attributes.role = 'alert'
if (icon && Alert.iconTypes[type]) {
div.innerHTML = Alert.icon(type)
}
const span = div.appendChild(document.createElement('span'))
span.classList.add('message')
span.innerHTML = message
return div
}
}
export class Recommendations extends AbstractRenderer {
get name() { return 'recommendations' }
render() {
if (!this.model.recommendations) {
this.el.innerHTML = ''
return this.el.appendChild(Alert.container("No recommendations found. You must be logged in and have followed some titles.", "danger"))
}
let recStr = ''
for (let [manga, chapters] of this.model.recommendations.unreadChaptersGroupedByManga) {
if (chapters.length > 0) {
const chapter = chapters[chapters.length - 1]
const more = chapters.length >= 2 ? ` <em class="ml-1">(+${chapters.length - 1} more)</em>` : ''
const relativeDate = formatDistance(new Date(chapter.timestamp * 1000), new Date(), { addSuffix: true })
recStr += `
<div class="col-xl-3 col-md-4 col-sm-6 border-bottom p-2 text-left">
<div class="rounded sm_md_logo float-left mr-2">
<a href="${manga.url}">
<img class="rounded max-width" src="${manga.coverThumb}">
</a>
</div>
<div>
<div class="text-truncate py-0 mb-1 border-bottom">
<span class="fas fa-book fa-fw" aria-hidden="true" title=""></span>
<a class="manga_title" title="${manga.title}" href="${manga.url}">${manga.title}</a>
</div>
<p class="py-0 mb-1 row no-gutters align-items-center flex-nowrap">
<div class="col-auto">
<span class="col-auto px-0">
<span class="rounded flag flag-${chapter.language}"></span>
</span>
<a class="" href="${chapter.url}" data-chapter="${chapter.id}">${chapter.fullTitle}</a>
${more}
</div>
</p>
<p class="text-truncate py-0 mb-1"><span class="far fa-clock fa-fw " aria-hidden="true" title=""></span> ${relativeDate}</span></p>
</div>
</div>`
}
}
this.el.innerHTML = `<h2 class="text-left">Recommendations</h2><h3 class="text-left">Unread Follows</h3><div class="row no-gutters">${recStr}</div>`
const handler = (evt) => {
const chapter = evt.target.dataset.chapter || evt.currentTarget.dataset.chapter
if (chapter) {
evt.preventDefault()
evt.stopPropagation()
this.view.moveToChapter(parseInt(chapter), 1)
// this.model.setRenderer(this.model.settings.renderingMode)
}
}
this.el.querySelectorAll('a').forEach(c => c.addEventListener('click', handler, true))
return Promise.resolve()
}
pageStateHandler() { }
getChapterTitle(ch, numOnly) {
let title = ''
if (ch.volume) title += `Vol. ${ch.volume} `
if (ch.chapter) title += `Ch. ${ch.chapter} `
if (ch.title && !numOnly) title += `${ch.title}`
if (!title) title = 'Oneshot'
return title.trim()
}
}

View File

@ -0,0 +1,158 @@
import Resource from './Resource'
import Manga from './Manga'
import Group from './Group'
const VOL_SHORT = 'Vol.'
const CH_SHORT = 'Ch.'
const TITLE_ONESHOT = 'Oneshot'
const TITLE_EMPTY = '(Untitled)'
export default class Chapter extends Resource {
static get resourceType() { return 'chapter' }
constructor(data = {}, responseCode = -1, responseStatus = null) {
super(data, responseCode, responseStatus)
this.id = data.id
this.hash = data.hash
this.mangaId = data.mangaId
this.chapter = data.chapter
this.volume = data.volume
this.title = data.title
this.timestamp = data.timestamp
this.language = data.language
this.threadId = data.threadId || null
this.comments = data.comments
this.server = data.server
this.serverFallback = data.serverFallback
this.groupIds = (data.groups || []).map(g => g.id || g)
this.groupWebsite = data.groupWebsite
this.pages = data.pages || []
this.read = data.read || false
this.status = data.status
}
// derived data
get textTitle() {
return this.title || ((this.volume || this.chapter) ? TITLE_EMPTY : TITLE_ONESHOT)
}
get numberTitle() {
return [
this.volume ? `${VOL_SHORT} ${this.volume}` : '',
this.chapter ? `${CH_SHORT} ${this.chapter}` : '',
].filter(n => n).join(' ')
}
get fullTitle() {
return [
this.numberTitle,
this.title,
].filter(n => n).join(' ') || TITLE_ONESHOT
}
get manga() { return Manga.getResource(this.mangaId) }
get groups() { return this.groupIds.map(id => Group.getResource(id)) }
get totalPages() { return !this.isExternal ? this.pages.length : 1 }
get url() { return `/chapter/${this.id}` }
get isNotFound() { return this._response.code === 404 }
get isDeleted() { return this.status === 'deleted' }
get isDelayed() { return this.status === 'delayed' }
get isUnavailable() { return this.status === 'unavailable' }
get isRestricted() { return this.status === 'restricted' }
get isExternal() { return this.status === 'external' }
get isFullyLoaded() { return this.hash != null && this.server != null }
get isUnnumbered() { return !this.volume && !this.chapter }
get isLastChapter() { return this.manga && this.manga.isLastChapter(this) }
get nextChapterId() { if (this.manga) return this.manga.getNextChapterId(this.id) }
get prevChapterId() { if (this.manga) return this.manga.getPrevChapterId(this.id) }
get nextChapter() { return Chapter.getResource(this.nextChapterId) }
get prevChapter() { return Chapter.getResource(this.prevChapterId) }
get isNetworkServer() {
if (this._isNetworkServer == null) {
this._isNetworkServer = /mangadex\.network/.test(this.server || '')
}
return this._isNetworkServer
}
// methods
getPage(i) {
return (i >= 1 && i <= this.totalPages && !this.isExternal) ? this.pages[i - 1] : ''
}
getPageUrl(i) {
return this.server + this.hash + '/' + this.getPage(i)
}
getAllPageUrls() {
function serverOrUndefined(server, hash, pg) {
if (!server) {
return server;
}
return server + hash + '/' + pg;
}
return !this.isExternal ? this.pages.map(pg => [serverOrUndefined(this.server, this.hash, pg), serverOrUndefined(this.serverFallback, this.hash, pg)]) : []
}
isAlternativeOf(ch) {
return ch.chapter == this.chapter
&& (ch.volume == this.volume || ch.volume == '' || this.volume == '')
&& (!this.isUnnumbered || ch.title == this.title)
}
isSequentialWith(chapter) {
if (!chapter) {
return undefined
} else if (typeof chapter == 'number') {
chapter = Chapter.getResource(chapter)
}
const ch1 = parseFloat(this.chapter)
const ch2 = parseFloat(chapter.chapter)
const vol1 = parseInt(this.volume) || 0
const vol2 = parseInt(chapter.volume) || 0
if (isNaN(ch1) || isNaN(ch2) || this.isUnnumbered || chapter.isUnnumbered) {
return undefined
} else if (this.isAlternativeOf(chapter)) {
return undefined
} else if (Math.abs(ch1 - ch2).toFixed(1) <= 1.1) {
return true
} else if ((ch1 === 1 && vol1 === vol2 + 1) || (ch2 === 1 && vol2 === vol1 + 1)) {
return true
} else {
return false
}
}
hasSameGroupsWith(chapter) {
if (this === chapter) {
return true
} else if (this.groups.length !== chapter.groups.length) {
return false
}
for (let i = 0; i < this.groups.length; ++i) {
if (this.groups[i].id !== chapter.groups[i].id) {
return false
}
}
return true
}
static async load(id, params = {}, cache = true) {
const json = await super.load(`chapter/${id}`, params)
try {
json.data.groups.forEach(g => Group.fromJSON(g))
return this.fromJSON(json.data, json.code, json.status, cache)
} catch (error) {
console.error(error)
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
}
}
static async loadWithManga(id, params = {}, cache = true) {
const json = await super.load(`chapter/${id}`, Object.assign(params, { include: 'manga' }))
try {
json.data.chapter.groups.forEach(g => Group.fromJSON(g))
const chapter = this.fromJSON(json.data.chapter, json.code, json.status, cache)
Manga.fromJSON(json.data.manga, json.code, json.status, cache)
return chapter
} catch (error) {
console.error(error)
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
}
}
}

View File

@ -0,0 +1,36 @@
import Cookies from 'js-cookie'
import Resource from './Resource'
import Chapter from './Chapter'
import Manga from './Manga'
export default class Follows extends Resource {
static get resourceType() { return 'follows' }
constructor(data = {}, responseCode = -1, responseStatus = null) {
super(data, responseCode, responseStatus)
this.chapters = data.chapters.map(c => new Chapter(c)).sort((a, b) => b.timestamp - a.timestamp)
this.manga = new Map(Object.values(data.manga).map(m => [m.id, new Manga(m)]))
}
get unreadChapters() {
return this.chapters.filter(c => !c.read)
}
get unreadChaptersGroupedByManga() {
return this.unreadChapters.reduce((map, ch) => {
const manga = this.manga.get(ch.mangaId)
if (!map.has(manga)) {
map.set(manga, [])
}
map.get(manga).push(ch)
return map
}, new Map())
}
static async load(params = {}, cache = true) {
const type = 1 // Reading
const hentai = Cookies.get('mangadex_h_toggle')
const json = await super.load(`user/me/followed-updates`, Object.assign({ type, hentai }, params)) // type 1 = Reading
return this.fromJSON(json.data, json.code, json.status, cache)
}
}

View File

@ -0,0 +1,15 @@
import Resource from './Resource'
export default class Group extends Resource {
static get resourceType() { return 'group' }
constructor(data = {}, responseCode = -1, responseStatus = null) {
super(data, responseCode, responseStatus)
this.id = data.id
this.name = data.name
}
static async load(id, cache = true) {
const json = await super.load(`group/${id}`, {})
return this.fromJSON(json.data, json.code, json.status, cache)
}
}

View File

@ -0,0 +1,165 @@
import { differenceInHours } from 'date-fns'
import Resource from './Resource'
import Chapter from './Chapter'
import Group from './Group'
import Utils from '../utils'
import natsort from 'natsort'
export default class Manga extends Resource {
static get resourceType() { return 'manga' }
constructor(data = {}, responseCode = -1, responseStatus = null) {
super(data, responseCode, responseStatus)
this.id = data.id
this.title = data.title
this.language = data.publication ? data.publication.language : null
this.isHentai = data.isHentai
this.lastChapter = data.lastChapter || ''
this.lastVolume = data.lastVolume || ''
this.mainCover = data.mainCover
this.tags = data.tags || []
this.links = data.links || {}
this._uniqueChapters = []
this._chapters = []
this.chapterListLastLoaded = null
}
// derived data
get isLongStrip() { return this.tags.indexOf(36) !== -1 }
get isDoujinshi() { return this.tags.indexOf(7) !== -1 }
get url() {
const title = this.title.toLowerCase().replace(/&[0-9a-z]+;/gi, '').replace(/[^0-9a-z]/gi, ' ').split(' ').filter(t => t).join('-')
return `/title/${this.id}/${title}`
}
get coverThumb() { return `/images/manga/${this.id}.thumb.jpg` }
get isChapterListOutdated() { return differenceInHours(this.chapterListLastLoaded, new Date()) >= 1 }
// methods
isLastChapter(chapter) {
return this.lastChapter && (chapter.chapter === this.lastChapter) && (chapter.volume == this.lastVolume)
}
getChapter(id) {
return Chapter.getResource(id)
}
get chapterList() {
return this._chapters
}
get uniqueChapterList() {
return this._uniqueChapters
}
updateChapterList(baseChapter, restrictChLang = false) {
let chapters = Chapter.findResources(c => c.mangaId === this.id)
if (restrictChLang) {
chapters = chapters.filter(c => c.language === baseChapter.language)
}
const chapterIds = Manga.createSortedChapterIdList(chapters)
this._chapters = chapterIds.map(id => Chapter.getResource(id))
this._uniqueChapters = Manga.createUniqueChapterList(baseChapter, this._chapters)
}
getNextChapterId(id) {
const index = this._uniqueChapters.map(c => c.id).indexOf(id)
return (index >= 0 && index + 1 < this._uniqueChapters.length) ? this._uniqueChapters[index + 1].id : 0
}
getPrevChapterId(id) {
const index = this._uniqueChapters.map(c => c.id).indexOf(id)
return (index > 0) ? this._uniqueChapters[index - 1].id : -1
}
getAltChapters(id) {
const cur = Chapter.getResource(id)
return cur ? this.chapterList.filter(ch => cur.isAlternativeOf(ch)) : []
}
static createSortedChapterIdList(chapters) {
chapters = chapters.map(ch => {
return !ch ? null : {
id: ch.id,
timestamp: ch.timestamp,
chapter: ch.chapter,
volume: ch.volume,
group: ch.groupIds[0],
}
}).filter(c => c)
const sorter = natsort({ asc: true, insensitive: true })
// sort by timestamp desc
Utils.stableSort(chapters, (a, b) => sorter(b.timestamp, a.timestamp))
// sort by volume desc, so that vol null > vol number where ch are equal
Utils.stableSort(chapters, (a, b) => sorter(b.volume, a.volume))
// sort by group
Utils.stableSort(chapters, (a, b) => sorter(a.group, b.group))
// sort by chapter number
Utils.stableSort(chapters, (a, b) => sorter(a.chapter, b.chapter))
// add "ghost" prev vol numbers
let pv = '0'
for (let c of chapters) {
c.prevVolume = pv
pv = c.volume ? c.volume : pv
}
// sort by vol or prev vol
Utils.stableSort(chapters, (a, b) => sorter(a.volume || a.prevVolume, b.volume || b.prevVolume))
return chapters.map(c => c.id)
}
static createUniqueChapterList(baseChapter, chapters) {
const list = []
if (chapters.length > 0) {
let best = chapters[0]
for (let ch of chapters.slice(1)) {
if (!ch.isAlternativeOf(best)) {
list.push(best)
best = ch
} else if (best !== baseChapter && ch.hasSameGroupsWith(baseChapter)) {
best = ch
}
}
list.push(best)
}
return list
}
static async load(id, params = {}, cache = true) {
const json = await super.load(`manga/${id}`, params)
try {
return this.fromJSON(json.data, json.code, json.status, cache)
} catch (error) {
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
}
}
static async loadChapterList(id, params = {}, cache = true) {
const json = await super.load(`manga/${id}/chapters`, params)
try {
const { chapters } = this.fromChapterListJSON(id, json, cache)
return chapters
} catch (error) {
return []
}
}
static async loadWithChapterList(id, params = {}, cache = true) {
const json = await super.load(`manga/${id}`, Object.assign(params, { include: 'chapters' }))
try {
const { manga } = this.fromChapterListJSON(id, json, cache)
return manga
} catch (error) {
throw this.fromJSON({ id, status: json.message }, json.code, json.status, false)
}
}
static fromChapterListJSON(id, json, cache) {
// avoid overwriting group/chapter resources that already exist because these are going to be incomplete
const groups = json.data.groups.map(g => Group.getResource(g.id) || Group.fromJSON(g))
const chapters = json.data.chapters.map(c => Chapter.getResource(c.id) || Chapter.fromJSON(c, json.code, json.status, cache))
const manga = json.data.manga ? this.fromJSON(json.data.manga, json.code, json.status, cache) : this.getResource(id)
manga.chapterListLastLoaded = new Date()
//manga.updateChapterList()
return { groups, chapters, manga }
}
}

View File

@ -0,0 +1,62 @@
export default class Resource {
constructor(data = {}, responseCode = -1, responseStatus = null) {
this._response = {
code: responseCode,
status: responseStatus,
}
Resource.addResource(this)
}
static addResource(resource) {
const id = resource.id
const type = resource.constructor.resourceType
if (id && type) {
if (!(type in Resource.cache)) {
Resource.cache[type] = {}
}
Resource.cache[type][id] = resource
}
}
static getResource(id) {
try { return Resource.cache[this.resourceType][id] }
catch (e) { return null }
}
static findResources(filterFn) {
try { return Object.values(Resource.cache[this.resourceType]).filter(filterFn) }
catch (e) { return [] }
}
static fromJSON(data, responseCode = -1, responseStatus = null, cache = true) {
const resource = new this(data, responseCode, responseStatus)
if (cache) {
Resource.addResource(resource)
}
return resource
}
static async load(resourceURL, params = {}) {
try {
const url = new URL(process.env.API_URL + resourceURL)
for (let key in params) {
if (params[key] != null) {
url.searchParams.append(key, params[key])
}
}
const res = await fetch(url, {
credentials: 'include',
})
if (res.status >= 500) {
throw new Error("Error while loading a resource. The server may be busy at the moment.")
}
return await res.json()
} catch (e) {
console.error("Resource loading error:", e)
throw e
}
}
}
Resource.cache = {}

356
scripts/reader/ui.js Normal file
View File

@ -0,0 +1,356 @@
'use strict'
/* global Renderer */
const utils = {
empty: (node) => {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
}
class UI {
constructor(reader) {
this.reader = reader
}
get isSinglePage() { return this.renderingMode === UI.RENDERING_MODE.SINGLE }
get isDoublePage() { return this.renderingMode === UI.RENDERING_MODE.DOUBLE }
get isLongStrip() { return this.renderingMode === UI.RENDERING_MODE.LONG }
get isNoResize() { return this.displayFit === UI.DISPLAY_FIT.NO_RESIZE }
get isFitHeight() { return this.displayFit === UI.DISPLAY_FIT.FIT_HEIGHT }
get isFitWidth() { return this.displayFit === UI.DISPLAY_FIT.FIT_WIDTH }
get isFitBoth() { return this.displayFit === UI.DISPLAY_FIT.FIT_BOTH }
get isDirectionLTR() { return this.direction === UI.DIRECTION.LTR }
get isDirectionRTL() { return this.direction === UI.DIRECTION.RTL }
get renderedPages() {
if (this.renderer == null) {
return 0
} else if (this.isLongStrip) {
return 1
} else {
return this.renderer.renderedPages
}
}
initializeContainer(userIsGuest = false) {
this.container = document.querySelector('div[role="main"]')
this.container.classList.remove('container')
this.container.classList.add('reader', 'row', 'flex-column', 'flex-lg-row', 'no-gutters')
this.imageContainer = this.container.querySelector('.reader-images')
document.querySelector('footer').classList.add('d-none')
document.body.style.removeProperty('margin-bottom')
if (userIsGuest) {
const reportBtn = this.container.querySelector('#report-button')
reportBtn.dataset.toggle = ''
reportBtn.href = '/login'
reportBtn.firstElementChild.classList.replace('fa-flag', 'fa-sign-in-alt')
}
}
setRenderer(mode = this.reader.settings.renderingMode, useHistory = true) {
if (this.renderingMode !== mode) {
if (this.renderer != null) {
this.renderer.destroy()
}
this.renderingMode = mode
switch(mode) {
case UI.RENDERING_MODE.LONG:
this.renderer = new Renderer.LongStrip(this.reader)
break
case UI.RENDERING_MODE.DOUBLE:
this.renderer = new Renderer.DoublePage(this.reader)
break
case UI.RENDERING_MODE.SINGLE:
default:
this.renderer = new Renderer.SinglePage(this.reader)
break
case UI.RENDERING_MODE.ALERT:
this.renderer = new Renderer.Alert(this.reader)
break
case UI.RENDERING_MODE.RECS:
this.renderer = new Renderer.Recommendations(this.reader)
break
}
this.container.dataset.renderer = this.renderer.name
if (useHistory) {
this.pushHistory(this.reader.currentPage)
}
}
}
setDirection(direction) {
this.direction = direction
this.container.dataset.direction = UI.DIRECTION.LTR === direction ? 'ltr' : 'rtl'
}
setDisplayFit(fit) {
this.displayFit = fit
this.container.dataset.display = UI.DISPLAY_FIT_STR[fit]
this.container.classList.toggle('fit-horizontal', this.isFitBoth || this.isFitWidth)
this.container.classList.toggle('fit-vertical', this.isFitBoth || this.isFitHeight)
}
onChapterChange(chapter) {
if (this.renderer) {
this.renderer.initialize()
}
this.container.classList.toggle('native-long-strip', chapter.manga.isLongStrip)
this.setRenderer(chapter.manga.isLongStrip ? UI.RENDERING_MODE.LONG : this.reader.settings.renderer, false)
this.setDisplayFit(chapter.manga.isLongStrip ? UI.DISPLAY_FIT.FIT_WIDTH : this.reader.settings.displayFit)
this.resetPageBar(chapter.totalPages)
// TODO?: it's assumed that the input is already escaped; when not, change innerHTML to textContent
this.updateTitles(chapter)
this.updateChapterDropdown(chapter, !this.reader.settings.showDropdownTitles)
this.updatePageDropdown(chapter.totalPages, this.reader.currentPage)
this.updateGroupList(chapter)
this.updateCommentsButton(chapter)
this.updateChapterLinks(chapter)
}
updateSetting(key, value) {
switch (key) {
case 'direction':
this.setDirection(value)
this.updateChapterLinks()
this.setRenderer()
if (this.reader.currentPage) {
this.render(this.reader.currentPage)
}
break
case 'renderingMode':
this.setRenderer(value)
if (this.reader.currentPage) {
this.render(this.reader.currentPage)
}
break
case 'displayFit':
this.setDisplayFit(value)
break
case 'containerWidth':
if (!value) {
value = null
}
this.imageContainer.style.maxWidth = value ? `${value}px` : null
break
case 'showDropdownTitles':
this.updateChapterDropdown(this.reader.chapter, !value)
break
case 'hideHeader':
this.container.classList.toggle('hide-header', value)
document.querySelector('nav.navbar').classList.toggle('d-none', value)
document.querySelector('#fullscreen-button').classList.toggle('active', value)
break
case 'hideSidebar':
this.container.classList.toggle('hide-sidebar', value)
break
case 'hidePagebar':
this.container.classList.toggle('hide-page-bar', value)
break
}
Array.from(this.container.querySelectorAll(`#modal-settings input[data-setting="${key}"]`)).forEach(n => { n.value = value })
Array.from(this.container.querySelectorAll(`#modal-settings select[data-setting="${key}"]`)).forEach(n => { n.value = value })
Array.from(this.container.querySelectorAll(`#modal-settings button[data-setting="${key}"]`)).forEach(n => { n.classList.toggle('active', n.dataset.value == value) })
}
updateTitles(chapter) {
const manga = chapter.manga
document.title = `${manga.title} - ${manga.getChapterTitle(chapter.id)} - MangaDex`
const mangaFlag = this.container.querySelector('.reader-controls-title .lang-flag')
mangaFlag.parentElement.replaceChild(UI.flagImg(manga.langCode, manga.langName), mangaFlag)
const mangaLink = this.container.querySelector('.manga-link')
mangaLink.href = manga.url
mangaLink.title = manga.title
mangaLink.innerHTML = manga.title
this.container.querySelector('.chapter-title').innerHTML = chapter.title
this.container.querySelector('.chapter-tag-h').classList.toggle('d-none', !manga.isHentai)
this.container.querySelector('.chapter-tag-end').classList.toggle('d-none', !chapter.isLastChapter)
this.container.querySelector('.chapter-tag-doujinshi').classList.toggle('d-none', !manga.isDoujinshi)
}
updateChapterDropdown(chapter, hideTitles) {
if (chapter) {
const manga = chapter.manga
const chapters = this.container.querySelector('#jump-chapter')
utils.empty(chapters)
for (let ch of manga.chapterList.slice().reverse()) {
const option = chapters.appendChild(document.createElement('option'))
option.value = ch.id
option.selected = ch.id === chapter.id
option.appendChild(document.createTextNode(manga.getChapterTitle(ch.id, hideTitles)))
}
}
}
updatePageDropdown(totalPages, currentPage) {
const pages = this.container.querySelector('#jump-page')
utils.empty(pages)
for (let i = 1; i <= totalPages; ++i) {
const option = pages.appendChild(document.createElement('option'))
option.value = i
option.selected = currentPage === i
option.appendChild(document.createTextNode(i))
}
}
updateGroupList(chapter) {
const groups = this.container.querySelector('.reader-controls-groups ul')
utils.empty(groups)
for (let g of chapter.manga.getGroupsOfChapter(chapter.id)) {
const li = groups.appendChild(document.createElement('li'))
const flag = li.appendChild(UI.flagImg(g.lang_code, g.lang_code))
flag.classList.add('mr-1')
const link = li.appendChild(document.createElement(g.id == chapter.id ? 'b' : 'a'))
link.innerHTML = [g.group_name, g.group_name_2, g.group_name_3].filter(n => n).join(' | ')
link.dataset.chapter = g.id
link.href = `/chapter/${g.id}`
}
}
updateCommentsButton(chapter) {
this.container.querySelector('#comment-button').href = this.pageURL(chapter.id) + '/comments'
this.container.querySelector('.comment-amount').textContent = chapter.comments || ''
}
updateChapterLinks(chapter) {
if (chapter) {
const update = (toLeft) => {
if (this.direction != null) {
let id = (toLeft === this.isDirectionLTR) ? chapter.prevChapterId : chapter.nextChapterId
return (a) => {
a.dataset.chapter = id
a.href = this.pageURL(id)
a.title = chapter.manga.getChapterTitle(id) || 'Back to manga'
}
}
}
Array.from(this.container.querySelectorAll('a.chapter-link-left')).forEach(update(true))
Array.from(this.container.querySelectorAll('a.chapter-link-right')).forEach(update(false))
}
}
updatePageLinks(chapter, pg) {
if (chapter && typeof pg === 'number') {
const pgStr = `${pg}${this.renderedPages===2 ? ` - ${pg + 1}` : ''}`
this.container.querySelector('.reader-controls-pages .current-page').textContent = pgStr
this.container.querySelector('.reader-controls-pages .total-pages').textContent = chapter.totalPages
this.container.querySelector('.reader-controls-pages .page-link-left').href = this.pageLeftURL(chapter.id, 1)
this.container.querySelector('.reader-controls-pages .page-link-right').href = this.pageRightURL(chapter.id, 1)
this.container.querySelector('#jump-page').value = pg
}
}
pageURL(id, pg) {
if (id != null && id > 0) {
if (pg != null) {
if (pg === 0) {
return this.pageURL(this.reader.chapter.prevChapterId, -1)
} else if (pg > this.reader.chapter.totalPages) {
return this.pageURL(this.reader.chapter.nextChapterId)
}
return this.isTestReader ? `/?page=chapter_test&id=${id}&p=${pg}` : `/chapter/${id}/${pg}`
}
return this.isTestReader ? `/?page=chapter_test&id=${id}` : `/chapter/${id}`
}
return this.manga.url
}
pageLeftURL(id, pages = this.isDoublePage ? 2 : 1) {
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? -pages : pages)), 0)
}
pageRightURL(id, pages = this.isDoublePage ? 2 : 1) {
return this.pageURL(id, Math.min(this.reader.currentPage + (this.isDirectionLTR ? pages : -pages)), 0)
}
resetPageBar(totalPages) {
if (totalPages) {
const notches = this.container.querySelector('.reader-page-bar .notches')
utils.empty(notches)
for (let i = 1; i <= totalPages; ++i) {
const notch = notches.appendChild(document.createElement('div'))
notch.classList.add('notch', 'col')
notch.style.order = i
notch.dataset.page = i
notch.title = `Page ${i}`
// notch.classList.toggle('trail', i <= pg-this.renderedPages)
// notch.classList.toggle('thumb', i > pg-this.renderedPages && i <= pg)
}
this.updatePageBar(totalPages, 1)
}
}
updatePageBar(totalPages, pg) {
if (totalPages && typeof pg === 'number') {
const trail = this.container.querySelector('.reader-page-bar .trail')
const thumb = this.container.querySelector('.reader-page-bar .thumb')
const notchSize = 100 / Math.max(totalPages, 1)
trail.style.width = Math.min(pg * notchSize, 100) + '%'
thumb.style.width = (100 / pg * Math.max(this.reader.renderedPages, 1)) + '%'
trail.style.right = this.reader.isDirectionLTR ? null : 0
thumb.style.float = this.reader.isDirectionLTR ? 'right' : 'left'
}
}
render(chapter, pg) {
if (!pg || chapter == null || this.ui.renderer == null) {
return Promise.reject()
}
return this.renderer.render(pg).then(() => {
this.updatePageLinks(chapter, this.reader.currentPage)
this.updatePageBar(chapter, this.reader.currentPage + this.renderedPages - 1)
}).catch((err) => this.renderError(err))
}
renderError(err) {
console.error('Render error', err)
this.setRenderer(UI.RENDERING_MODE.ALERT)
return this.renderer.render({ type: 'danger', 'message': err.message, 'err': err })
}
static flagImg (langCode = 'jp', langName = 'Unknown') {
const flag = document.createElement('img')
flag.classList.add('lang-flag')
flag.src = `https://mangadex.org/images/flags/${langCode}.png`
flag.alt = langName
flag.title = langName
return flag
}
}
UI.RENDERING_MODE = {
SINGLE: 1,
DOUBLE: 2,
LONG: 3,
ALERT: 4,
RECS: 5,
}
UI.DIRECTION = {
LTR: 1,
RTL: 2,
}
UI.DISPLAY_FIT = {
FIT_BOTH: 1,
FIT_WIDTH: 2,
FIT_HEIGHT: 3,
NO_RESIZE: 4,
}
UI.DISPLAY_FIT_STR = {
1: 'fit-both',
2: 'fit-width',
3: 'fit-height',
4: 'no-resize',
}
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = UI
} else if (typeof define === 'function' && define.amd) {
define([], function () {
return UI
})
} else if (typeof exports === 'object') {
exports.UI = UI
} else {
(typeof window !== 'undefined' ? window : this).UI = UI
}

69
scripts/reader/utils.js Normal file
View File

@ -0,0 +1,69 @@
export default class Utils {
static range (start, end, step = 1) {
const r = []
for (let i = start; i < end; i += step) {
r.push(i)
}
return r
}
static clamp (val, min, max) {
return Math.max(min, Math.min(val, max))
}
static emptyNode (node) {
while (node && node.firstChild) {
node.removeChild(node.firstChild)
}
}
static scrollBy () {
window.scrollBy.apply(null, arguments)
}
static stableSort (array, cmp) {
// https://medium.com/@fsufitch/is-javascript-array-sort-stable-46b90822543f
cmp = !!cmp ? cmp : (a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
}
const stabilized = array.map((el, index) => [el, index])
const stableCmp = (a, b) => {
const order = cmp(a[0], b[0])
return order != 0 ? order : a[1] - b[1]
}
stabilized.sort(stableCmp)
for (let i = 0; i < array.length; ++i) {
array[i] = stabilized[i][0]
}
return array
}
static htmlTextDecodeHack(str) {
const textarea = document.createElement('textarea')
textarea.innerHTML = str
return textarea.value
}
}
try {
window.scrollBy({ top: 0, behavior: "smooth" })
} catch(e) {
Utils.scrollBy = function() {
const arg = arguments[0]
switch(typeof arg) {
case 'object':
return window.scrollBy(arg.top || 0, arg.left || 0)
case 'number':
return window.scrollBy.apply(null, arguments)
}
}
}
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = Utils
}

58
sql.php
View File

@ -2,9 +2,22 @@
if (PHP_SAPI !== 'cli') if (PHP_SAPI !== 'cli')
die(); die();
require_once ('/home/www/mangadex.org/bootstrap.php'); //must be like this require_once ('/var/www/mangadex.org/bootstrap.php'); //must be like this
require_once (ABSPATH . "/scripts/header.req.php"); require_once (ABSPATH . "/scripts/header.req.php");
/*
for ($id = 0; $id < 2491159; $id++) {
$memcached->delete("user_{$id}_friends_user_ids");
$memcached->delete("user_{$id}_pending_friends_user_ids");
$memcached->delete("user_{$id}_friends_user_ids");
$memcached->delete("user_{$id}_pending_friends_user_ids");
if ($id % 1000 === 0) echo ".";
}
die("\nend\n");
*/
/* /*
$result = $sql->query_read('x', " SELECT COUNT(*) AS `Rows`, `user_id` FROM `mangadex_clients` where approved = 1 GROUP BY `user_id` ORDER BY `user_id` ", 'fetchAll', PDO::FETCH_ASSOC, -1); $result = $sql->query_read('x', " SELECT COUNT(*) AS `Rows`, `user_id` FROM `mangadex_clients` where approved = 1 GROUP BY `user_id` ORDER BY `user_id` ", 'fetchAll', PDO::FETCH_ASSOC, -1);
@ -154,7 +167,7 @@ foreach ($txs as $tx) {
} }
*/ */
/* /*
$joined_timestamp = $sql->query_read('x', " SELECT joined_timestamp FROM mangadex_users WHERE joined_timestamp >= 1594166400 ", 'fetchAll', PDO::FETCH_COLUMN, -1); $joined_timestamp = $sql->query_read('x', " SELECT joined_timestamp FROM mangadex_users WHERE joined_timestamp >= 1597622400 ", 'fetchAll', PDO::FETCH_COLUMN, -1);
foreach($joined_timestamp as $value) { foreach($joined_timestamp as $value) {
$date = date('Y-m-d', $value); $date = date('Y-m-d', $value);
@ -259,19 +272,19 @@ foreach ($results as $ro) {
/* /*
$dir = '/home/www/mangadex.org/data/'; $dir = '/var/www/mangadex.org/data/';
$files = array_diff(scandir($dir), array('..', '.')); $files = array_diff(scandir($dir), array('..', '.'));
foreach ($files as $file) { foreach ($files as $file) {
$chapter_id = $sql->query_read('chapter_id', " SELECT chapter_id FROM mangadex_chapters WHERE chapter_hash LIKE '$file' ", 'fetchColumn', '', -1); $chapter_id = $sql->query_read('chapter_id', " SELECT chapter_id FROM mangadex_chapters WHERE chapter_hash LIKE '$file' ", 'fetchColumn', '', -1);
if ($chapter_id) { if (!$chapter_id) {
print $file . " - $chapter_id\n"; print $file . " - $chapter_id\n";
$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]); //$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
$memcached->delete("chapter_$chapter_id"); //$memcached->delete("chapter_$chapter_id");
//rename("/home/www/mangadex.org/data/$file", "/home/www/mangadex.org/delete/$file"); rename("/var/www/mangadex.org/data/$file", "/var/www/mangadex.org/delete/$file");
} }
} }
@ -338,7 +351,7 @@ foreach ($array as $manga) {
} }
*/ */
/*
$results = $sql->query_read('x', " SELECT * FROM mangadex_users where level_id = 0 and user_id > 1900000 order by user_id desc ", 'fetchAll', PDO::FETCH_ASSOC, -1); $results = $sql->query_read('x', " SELECT * FROM mangadex_users where level_id = 0 and user_id > 1900000 order by user_id desc ", 'fetchAll', PDO::FETCH_ASSOC, -1);
foreach ($results as $row) { foreach ($results as $row) {
$uid = $row['user_id']; $uid = $row['user_id'];
@ -348,7 +361,7 @@ foreach ($results as $row) {
print $uid . ' '; print $uid . ' ';
} }
*/
/* /*
foreach (WALLET_QR['ETH'] as $qr) { foreach (WALLET_QR['ETH'] as $qr) {
print "Fetching $qr\n\n"; print "Fetching $qr\n\n";
@ -409,12 +422,30 @@ foreach ($txs as $tx) {
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1); //$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
/*
$result = $sql->query_read('x', " SELECT * FROM `mangadex_chapters` WHERE `manga_id` = 47 AND `server` = 1 AND `chapter_deleted` = 1 ", 'fetchAll', PDO::FETCH_ASSOC, -1); //$result = $sql->query_read('x', " SELECT * FROM `mangadex_chapters` WHERE `manga_id` = 47 AND `server` = 0 AND `chapter_deleted` = 1 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
$result = $sql->query_read('x', "
SELECT * FROM `mangadex_chapters` WHERE `upload_timestamp` > 1604707200 AND upload_timestamp < 1605312000 and `group_id` != 9097 AND `server` = 0 AND `chapter_deleted` = 0
", 'fetchAll', PDO::FETCH_ASSOC, -1);
foreach ($result as $row) { foreach ($result as $row) {
print $row['chapter_hash'] . ' '; //print $row['chapter_hash'] . ' ';
}*/ $file = $row['chapter_hash'];
$chapter_id = $row['chapter_id'];
$memcached->delete("chapter_$chapter_id");
print "$file - $chapter_id\n";
$sql->modify('update', " UPDATE mangadex_chapters SET server = 1 WHERE chapter_id = ? LIMIT 1; ", [$chapter_id]);
//$memcached->delete("chapter_$chapter_id");
rename("/var/www/mangadex.org/data/$file", "/var/www/mangadex.org/transferred/$file");
}
//$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1); //$result = $sql->query_read('x', " SELECT chapters.*, users.level_id, users.user_id, users.username FROM `mangadex_chapters` as chapters left join mangadex_users as users on chapters.user_id = users.user_id where users.level_id = 0 and chapters.chapter_deleted = 1 and chapters.server = 0 ", 'fetchAll', PDO::FETCH_ASSOC, -1);
@ -482,3 +513,4 @@ foreach ($result as $row) {
//var_dump(is_banned_asn('177.100.112.109')); //var_dump(is_banned_asn('177.100.112.109'));
//var_dump(get_asn('177.100.112.109')); //var_dump(get_asn('177.100.112.109'));

View File

@ -10,7 +10,7 @@ use Mangadex\Exception\Http\UnavailableForLegalReasonsHttpException;
class ChapterController extends APIController class ChapterController extends APIController
{ {
const CHAPTERS_LIMIT = 6000; const CHAPTERS_LIMIT = 8000;
const CH_STATUS_OK = 'OK'; const CH_STATUS_OK = 'OK';
const CH_STATUS_DELETED = 'deleted'; const CH_STATUS_DELETED = 'deleted';
const CH_STATUS_DELAYED = 'delayed'; const CH_STATUS_DELAYED = 'delayed';
@ -36,10 +36,9 @@ class ChapterController extends APIController
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
$id = $this->validateId($id); $id = $this->validateId($id);
@ -53,6 +52,13 @@ class ChapterController extends APIController
if (isset($normalized['pages'])) { if (isset($normalized['pages'])) {
$this->updateChapterViews($chapter); $this->updateChapterViews($chapter);
} }
if (in_array('manga', $this->request->query->getList('include'))) {
$mangaController = new MangaController();
$manga = $mangaController->normalize($mangaController->fetch($normalized['mangaId']));
$normalized = ['chapter' => $normalized, 'manga' => $manga];
}
return $normalized; return $normalized;
} }
@ -87,6 +93,13 @@ class ChapterController extends APIController
throw new BadRequestHttpException("Invalid limit, range must be within 10 - 100."); throw new BadRequestHttpException("Invalid limit, range must be within 10 - 100.");
} }
if ($this->request->query->getBoolean('blockgroups', true)) {
$blockedGroups = $this->user->get_blocked_groups();
if ($blockedGroups) {
$search['blocked_groups'] = array_keys($blockedGroups);
}
}
$chapters = new \Chapters($search); $chapters = new \Chapters($search);
$list = $chapters->query_read($order, self::CHAPTERS_LIMIT, 1); $list = $chapters->query_read($order, self::CHAPTERS_LIMIT, 1);
if ($page > 0) { if ($page > 0) {
@ -153,9 +166,11 @@ class ChapterController extends APIController
if (!empty($langFilter)) { if (!empty($langFilter)) {
$search["multi_lang_id"] = $langFilter; $search["multi_lang_id"] = $langFilter;
} }
$blockedGroups = $this->user->get_blocked_groups(); if ($this->request->query->getBoolean('blockgroups', true)) {
if ($blockedGroups) { $blockedGroups = $this->user->get_blocked_groups();
$search['blocked_groups'] = array_keys($blockedGroups); if ($blockedGroups) {
$search['blocked_groups'] = array_keys($blockedGroups);
}
} }
if ($hentai !== 1) { // i.e. if hentai is 0 (hide) or >1 (show only) if ($hentai !== 1) { // i.e. if hentai is 0 (hide) or >1 (show only)
$search['manga_hentai'] = $hentai ? 1 : 0; $search['manga_hentai'] = $hentai ? 1 : 0;
@ -174,13 +189,25 @@ class ChapterController extends APIController
$chaptersResult = $chapters->query_read($order, $limit, max($page, 1)); $chaptersResult = $chapters->query_read($order, $limit, max($page, 1));
$normalized = $this->normalizeList($chaptersResult, false); $normalized = $this->normalizeList($chaptersResult, false);
if ($userResource->user_id === $this->user->user_id) {
$readChapters = $this->user->get_read_chapters();
foreach ($normalized['chapters'] as &$chapter) {
$chapter['read'] = in_array(
$chapter['id'],
$readChapters
);
}
}
// include basic manga entities // include basic manga entities
$manga = []; $manga = [];
foreach ($chaptersResult as $chapter) { foreach ($chaptersResult as &$chapter) {
if (!isset($manga[$chapter['manga_id']])) { if (!isset($manga[$chapter['manga_id']])) {
$manga[$chapter['manga_id']] = [ $manga[$chapter['manga_id']] = [
'id' => $chapter['manga_id'], 'id' => $chapter['manga_id'],
// TODO: remove 'name'
'name' => $chapter['manga_name'], 'name' => $chapter['manga_name'],
'title' => $chapter['manga_name'],
'isHentai' => (bool)$chapter['manga_hentai'], 'isHentai' => (bool)$chapter['manga_hentai'],
'lastChapter' => (!empty($chapter['manga_last_chapter']) && $chapter['manga_last_chapter'] !== '0') ? $chapter['manga_last_chapter'] : null, 'lastChapter' => (!empty($chapter['manga_last_chapter']) && $chapter['manga_last_chapter'] !== '0') ? $chapter['manga_last_chapter'] : null,
'lastVolume' => (string)$chapter['manga_last_volume'] ?: null, 'lastVolume' => (string)$chapter['manga_last_volume'] ?: null,
@ -208,6 +235,7 @@ class ChapterController extends APIController
'groups' => [], 'groups' => [],
'uploader' => $chapter->user_id, 'uploader' => $chapter->user_id,
'timestamp' => $chapter->upload_timestamp, 'timestamp' => $chapter->upload_timestamp,
'threadId' => $chapter->thread_id,
'comments' => $chapter->thread_posts ?? 0, 'comments' => $chapter->thread_posts ?? 0,
'views' => $chapter->chapter_views ?? 0, 'views' => $chapter->chapter_views ?? 0,
]; ];
@ -218,36 +246,58 @@ class ChapterController extends APIController
}); });
$normalized['groups'] = array_map(function ($g) { $normalized['groups'] = array_map(function ($g) {
return ['id' => $g[0], 'name' => $g[1]]; return ['id' => $g[0], 'name' => $g[1]];
}, $groupsFiltered); }, array_values($groupsFiltered));
if ($fullData) { if ($fullData) {
$isValidated = validate_level($this->user, 'pr')
|| $this->request->headers->get("API_KEY") === PRIVATE_API_KEY;
$normalized['status'] = self::CH_STATUS_OK; $normalized['status'] = self::CH_STATUS_OK;
$isExternal = substr($chapter->page_order, 0, 4) === 'http'; $isExternal = substr($chapter->page_order, 0, 4) === 'http';
$isRestricted = in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) &&
!validate_level($this->user, 'contributor') &&
!$hasPrivateAuth &&
$this->user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA;
$countryCode = strtoupper(get_country_code($this->user->last_ip));
$isRegionBlocked = isset(REGION_BLOCKED_MANGA[$countryCode]) &&
in_array($chapter->manga_id, REGION_BLOCKED_MANGA[$countryCode]) &&
!$isValidated;
// Set status when something other than OK // Set status when something other than OK
if ($chapter->chapter_deleted) { if ($chapter->chapter_deleted) {
if (!validate_level($this->user, 'pr')) { if (!$isValidated) {
throw new GoneHttpException(self::CH_STATUS_DELETED); throw new GoneHttpException(self::CH_STATUS_DELETED);
} }
$normalized['status'] = self::CH_STATUS_DELETED; $normalized['status'] = self::CH_STATUS_DELETED;
} else if (!$chapter->available) { } else if (!$chapter->available) {
if (!validate_level($this->user, 'pr')) { if (!$isValidated) {
throw new UnavailableForLegalReasonsHttpException(self::CH_STATUS_UNAVAILABLE); throw new UnavailableForLegalReasonsHttpException(self::CH_STATUS_UNAVAILABLE);
} }
$normalized['status'] = self::CH_STATUS_UNAVAILABLE; $normalized['status'] = self::CH_STATUS_UNAVAILABLE;
$normalized['groups'] = []; $normalized['groups'] = [];
} else if ($chapter->upload_timestamp > time()) { } else if ($chapter->upload_timestamp > time()) {
if (!$isValidated) {
$groupLeaderIds = [$chapter->group_leader_id, $chapter->group_leader_id_2, $chapter->group_leader_id_3];
$isValidated = in_array($this->user->user_id, array_filter($groupLeaderIds, function ($n) {
return $n > 0;
}));
}
if (!$isValidated) {
$groups = array_map(function ($g) {
return new \Group($g['id']);
}, $normalized['groups']);
$groupMemberIds = array_reduce($groups, function ($acc, $g) {
return array_merge($acc, array_keys($g->get_members()));
}, []);
$isValidated = in_array($this->user->user_id, $groupMemberIds);
}
$normalized['status'] = self::CH_STATUS_DELAYED; $normalized['status'] = self::CH_STATUS_DELAYED;
$normalized['groupWebsite'] = $chapter->group_website ?: null; $normalized['groupWebsite'] = $chapter->group_website ?: null;
} else if ($isExternal) { } else if ($isExternal) {
$normalized['status'] = self::CH_STATUS_EXTERNAL; $normalized['status'] = self::CH_STATUS_EXTERNAL;
$normalized['pages'] = $chapter->page_order; $normalized['pages'] = $chapter->page_order;
} else if ( } else if ($isRestricted || $isRegionBlocked) {
in_array($chapter->manga_id, RESTRICTED_MANGA_IDS) && if (!$isValidated) {
!validate_level($this->user, 'contributor') &&
$this->user->get_chapters_read_count() < MINIMUM_CHAPTERS_READ_FOR_RESTRICTED_MANGA
) {
if (!validate_level($this->user, 'pr')) {
throw new ForbiddenHttpException(self::CH_STATUS_RESTRICTED); throw new ForbiddenHttpException(self::CH_STATUS_RESTRICTED);
} }
$normalized = [ $normalized = [
@ -257,36 +307,20 @@ class ChapterController extends APIController
} }
// Include page information for non-external chapters and only for non-restricted users // Include page information for non-external chapters and only for non-restricted users
if (!$isExternal && ($normalized['status'] === self::CH_STATUS_OK || validate_level($this->user, 'pr'))) { if (!$isExternal && ($normalized['status'] === self::CH_STATUS_OK || $isValidated)) {
$pages = explode(',', $chapter->page_order); $pages = explode(',', $chapter->page_order);
$serverFallback = LOCAL_SERVER_URL; $serverFallback = IMG_SERVER_URL;
$serverNetwork = null; $serverNetwork = null;
// when a chapter does not exist on the local webserver, it gets an id
// since all imageservers share the same data, we can assign any imageserver with the best location to the user // use md@h for all images
if ($chapter->server > 0) { try {
if ($this->user->md_at_home ?? false) { $subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP, $this->user->mdh_portlimit ?? false);
try { if (!empty($subsubdomain)) {
$subsubdomain = $this->mdAtHomeClient->getServerUrl($chapter->chapter_hash, $pages, _IP); $serverNetwork = $subsubdomain;
if (!empty($subsubdomain)) {
$serverNetwork = $subsubdomain;
}
} catch (\Throwable $t) {
trigger_error($t->getMessage(), E_USER_WARNING);
}
}
$serverId = -1;
if ($this->request->query->has('server')) {
// if the parameter was trash, this returns -1
$serverId = get_server_id_by_code($this->request->query->get('server'));
}
if ($serverId < 1) {
// try to select a region-based server if we haven't one set already
$serverId = get_server_id_by_geography();
}
if ($serverId > 0) {
$serverFallback = "https://s$serverId.mangadex.org";
} }
} catch (\Throwable $t) {
trigger_error($t->getMessage(), E_USER_WARNING);
} }
$server = $serverNetwork ?: $serverFallback; $server = $serverNetwork ?: $serverFallback;
$dataDir = $this->request->query->getBoolean('saver') ? '/data-saver/' : '/data/'; $dataDir = $this->request->query->getBoolean('saver') ? '/data-saver/' : '/data/';

View File

@ -8,10 +8,9 @@ class FollowsController extends APIController
{ {
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
if (!empty($id)) { if (!empty($id)) {
throw new NotFoundHttpException(); throw new NotFoundHttpException();

View File

@ -8,10 +8,9 @@ class GroupController extends APIController
{ {
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
$id = $this->validateId($id); $id = $this->validateId($id);

View File

@ -0,0 +1,42 @@
<?php
namespace Mangadex\Controller\API;
use Mangadex\Exception\Http\ForbiddenHttpException;
class HighestChapterIDController extends APIController
{
public function view($path)
{
$isAuthorized = validate_level($this->user, 'pr') || $this->request->headers->get("API_KEY") === PRIVATE_API_KEY;
if (!$isAuthorized) {
throw new ForbiddenHttpException("You are not authorized.");
}
$id = $this->fetch();
$normalized = $this->normalize($id);
return $normalized;
}
public function fetch()
{
global $sql;
return $sql->prep(
"latest_chapter_id",
" SELECT MAX(chapter_id) FROM mangadex_chapters",
[],
'fetchColumn',
'',
-1
);
}
protected function normalize($id)
{
$normalized = [
'id' => $id,
];
return $normalized;
}
}

View File

@ -8,7 +8,7 @@ class IndexController extends APIController
{ {
return [ return [
"information" => "Authentication is achieved by the same means as logging in to the site (i.e. the mangadex_session, mangadex_rememberme_token cookies, correct User-Agent). Some chapters may require authenticated permissions to access. The Content-Type header for requests with bodies must be application/json, and the content must be valid JSON. Boolean query values are evaluated 1/true/on/yes for true, otherwise false.", "information" => "Authentication is achieved by the same means as logging in to the site (i.e. the mangadex_session, mangadex_rememberme_token cookies, correct User-Agent). Some chapters may require authenticated permissions to access. The Content-Type header for requests with bodies must be application/json, and the content must be valid JSON. Boolean query values are evaluated 1/true/on/yes for true, otherwise false.",
"baseUrl" => URL . "api/v2", "baseUrl" => defined('API_V2_URL') ? API_V2_URL : "https://api.mangadex.org/v2/",
"resources" => [ "resources" => [
"GET /" => [ "GET /" => [
"description" => "The current page, the API index.", "description" => "The current page, the API index.",
@ -29,6 +29,7 @@ class IndexController extends APIController
"queryParameters" => [ "queryParameters" => [
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.", "p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.", "limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
], ],
], ],
"GET /manga/{id}/covers" => [ "GET /manga/{id}/covers" => [
@ -61,6 +62,7 @@ class IndexController extends APIController
"queryParameters" => [ "queryParameters" => [
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.", "p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.", "limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
], ],
], ],
], ],
@ -79,13 +81,18 @@ class IndexController extends APIController
"queryParameters" => [ "queryParameters" => [
"p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.", "p" => "(Optional) The current page of the paginated results, starting from 1. Integer, default disables pagination.",
"limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.", "limit" => "(Optional) The limit of the paginated results, allowed range 10 - 100. Integer, default 100.",
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
], ],
], ],
"GET /user/{id}/settings" => [ "GET /user/{id}/settings" => [
"description" => "(Authorization required) Get a user's website settings.", "description" => "(Authorization required) Get a user's website settings.",
], ],
"GET /user/{id}/followed-manga" => [ "GET /user/{id}/followed-manga" => [
"description" => "(Authorization required) Get a user's followed manga and personal data for them.", "description" => "(Authorization required) Get a user's followed manga and personal data for them. The target user's MDList privacy setting is taken into account when determining authorization.",
"queryParameters" => [
"type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.",
"hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.",
],
], ],
"GET /user/{id}/followed-updates" => [ "GET /user/{id}/followed-updates" => [
"description" => "(Authorization required) Get the latest uploaded chapters for the manga that the user has followed, as well as basic related manga information. Ordered by timestamp descending (the datetime when the chapter is available). Limit 100 chapters per page. Note that the results are automatically filtered by the authorized user's chapter language filter setting.", "description" => "(Authorization required) Get the latest uploaded chapters for the manga that the user has followed, as well as basic related manga information. Ordered by timestamp descending (the datetime when the chapter is available). Limit 100 chapters per page. Note that the results are automatically filtered by the authorized user's chapter language filter setting.",
@ -94,6 +101,7 @@ class IndexController extends APIController
"type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.", "type" => "(Optional) Filter the results by the follow type ID (i.e. 1 = Reading, 2 = Completed etc). Use 0 to remove filtering. Integer, default 0.",
"hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.", "hentai" => "(Optional) Filter results based on whether the titles are marked as hentai. 0 = Hide H, 1 = Show all, 2 = Show H only. Integer, default 0.",
"delayed" => "(Optional) Include delayed chapters in the results. Boolean, default false.", "delayed" => "(Optional) Include delayed chapters in the results. Boolean, default false.",
"blockgroups" => "(Optional) Do not include chapters by groups blocked by the user. Boolean, default true.",
//"langs" => "(Optional) Filter results based on the scanlation language. Use a comma-separated list of language IDs.", //"langs" => "(Optional) Filter results based on the scanlation language. Use a comma-separated list of language IDs.",
], ],
], ],

View File

@ -8,10 +8,9 @@ class MangaController extends APIController
{ {
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
$id = $this->validateId($id); $id = $this->validateId($id);
@ -45,20 +44,22 @@ class MangaController extends APIController
return $manga; return $manga;
} }
private function normalize($manga) public function normalize($manga)
{ {
$coverPath = "/images/manga/$manga->manga_id.$manga->manga_image";
$normalized = [ $normalized = [
//'type' => 'manga', //'type' => 'manga',
'id' => $manga->manga_id, 'id' => $manga->manga_id,
'title' => $manga->manga_name, 'title' => $manga->manga_name,
'altTitles' => array_map(function ($alt_name) { 'altTitles' => array_map(function ($alt_name) {
return \html_entity_decode($alt_name); return trim(\html_entity_decode($alt_name));
}, $manga->get_manga_alt_names()), }, $manga->get_manga_alt_names()),
'description' => $manga->manga_description, 'description' => $manga->manga_description,
'artist' => explode(',', $manga->manga_artist), 'artist' => array_map(function ($a) {
'author' => explode(',', $manga->manga_author), return trim($a);
}, explode(',', $manga->manga_artist)),
'author' => array_map(function ($a) {
return trim($a);
}, explode(',', $manga->manga_author)),
'publication' => [ 'publication' => [
'language' => $manga->lang_flag, 'language' => $manga->lang_flag,
'status' => $manga->manga_status_id, 'status' => $manga->manga_status_id,
@ -74,7 +75,7 @@ class MangaController extends APIController
'id' => $relation['related_manga_id'], 'id' => $relation['related_manga_id'],
'title' => $relation['manga_name'], 'title' => $relation['manga_name'],
'type' => $relation['relation_id'], 'type' => $relation['relation_id'],
'isHentai' => (bool)$relation['hentai'], 'isHentai' => (bool)$relation['manga_hentai'],
]; ];
}, $manga->get_related_manga()), }, $manga->get_related_manga()),
'rating' => [ 'rating' => [

View File

@ -8,10 +8,9 @@ class RelationTypeController extends APIController
{ {
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
if (!empty($id)) { if (!empty($id)) {
throw new NotFoundHttpException(); throw new NotFoundHttpException();

View File

@ -8,10 +8,9 @@ class TagController extends APIController
{ {
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
if ($id) { if ($id) {
$id = $this->validateId($id); $id = $this->validateId($id);

View File

@ -24,12 +24,26 @@ class UserController extends APIController
return $id == $this->user->user_id || ($level !== null && validate_level($this->user, $level)); return $id == $this->user->user_id || ($level !== null && validate_level($this->user, $level));
} }
protected function isAuthorizedUserForMangaList($targetUser, $level)
{
if ($this->isAuthorizedUser($targetUser->user_id, $level)) {
return true;
} else if ($targetUser->list_privacy === 1) {
return true;
} else if ($targetUser->list_privacy === 2) {
$friends = $targetUser->get_friends_user_ids();
if ($friends[$this->user->user_id]['accepted'] ?? false) {
return true;
}
}
return false;
}
public function view($path) public function view($path)
{ {
/** $id = $path[0] ?? null;
* @param array{0: int|string, 1: string|null, 2: int|string|mixed|null} $path $subResource = $path[1] ?? null;
*/ $subResourceId = $path[2] ?? null;
[$id, $subResource, $subResourceId] = $path;
$id = $this->validateId($id); $id = $this->validateId($id);
@ -38,7 +52,8 @@ class UserController extends APIController
$this->fetch($id); // check if exists $this->fetch($id); // check if exists
return (new ChapterController())->fetchForUser($id); return (new ChapterController())->fetchForUser($id);
case 'followed-manga': case 'followed-manga':
if (!$this->isAuthorizedUser($id)) { $user = $this->fetch($id);
if (!$this->isAuthorizedUserForMangaList($user, 'mod')) {
throw new ForbiddenHttpException(); throw new ForbiddenHttpException();
} }
return $this->fetchFollowedManga($id); return $this->fetchFollowedManga($id);
@ -123,10 +138,13 @@ class UserController extends APIController
return [ return [
'userId' => $userId, 'userId' => $userId,
'mangaId' => $data['manga_id'], 'mangaId' => $data['manga_id'],
'mangaTitle' => $data['title'],
'isHentai' => (bool) $data['manga_hentai'],
'followType' => $data['follow_type'], 'followType' => $data['follow_type'],
'volume' => $data['volume'], 'volume' => $data['volume'],
'chapter' => $data['chapter'], 'chapter' => $data['chapter'],
'rating' => $data['rating'] ?: null, 'rating' => $data['rating'] ?: null,
'mainCover' => $data['manga_image'] ? $this->getFileUrl("/images/manga/{$data['manga_id']}.{$data['manga_image']}") : null,
]; ];
} }
@ -134,9 +152,21 @@ class UserController extends APIController
{ {
$userResource = $this->fetch($id); $userResource = $this->fetch($id);
$follows = $userResource->get_followed_manga_ids_api(); $follows = $userResource->get_followed_manga_ids_api();
return array_map(function ($data) use ($id) { if ($this->request->query->has('type')) {
$type = $this->request->query->getInt('type');
$follows = array_filter($follows, function ($m) use ($type) {
return $m['follow_type'] === $type;
});
}
$hentai = $this->request->query->getInt('hentai', 0);
if ($hentai !== 1) {
$follows = array_filter($follows, function ($m) use ($hentai) {
return $m['manga_hentai'] === 0 && $hentai === 0 || $m['manga_hentai'] === 1 && $hentai === 2;
});
}
return array_values(array_map(function ($data) use ($id) {
return $this->normalizeMangaUserData($id, $data); return $this->normalizeMangaUserData($id, $data);
}, $follows); }, $follows));
} }
public function fetchFollowedUpdates($id) public function fetchFollowedUpdates($id)
@ -148,7 +178,7 @@ class UserController extends APIController
public function fetchMangaUserData($id, $mangaId) public function fetchMangaUserData($id, $mangaId)
{ {
$userResource = $this->fetch($id); $userResource = $this->fetch($id);
$data = $userResource->get_manga_userdata($mangaId)[0] ?? null; $data = $userResource->get_manga_userdata($mangaId) ?? null;
if ($data === null) { if ($data === null) {
throw new NotFoundHttpException("Manga not found."); throw new NotFoundHttpException("Manga not found.");
} }
@ -171,20 +201,28 @@ class UserController extends APIController
public function fetchSettings($id) public function fetchSettings($id)
{ {
$user = $this->fetch($id); $user = $this->fetch($id);
$langIds = explode(',', $user->default_lang_ids ?? '');
$exludedTags = explode(',', $user->excluded_genres ?? '');
return [ return [
'id' => $user->user_id, 'id' => $user->user_id,
'hentaiMode' => $user->hentai_mode, 'hentaiMode' => $user->hentai_mode,
'latestUpdates' => $user->latest_updates, 'latestUpdates' => $user->latest_updates,
'showModeratedPosts' => (bool)$user->display_moderated, 'showModeratedPosts' => (bool)$user->display_moderated,
'showUnavailableChapters' => (bool)$user->show_unavailable, 'showUnavailableChapters' => (bool)$user->show_unavailable,
'shownChapterLangs' => explode(',', $user->default_lang_ids ?: ''), 'shownChapterLangs' => array_map(function ($id) {
'excludedTags' => explode(',', $user->excluded_genres ?: ''), return ['id' => $id];
}, $langIds),
'excludedTags' => array_map(function ($id) {
return ['id' => (int)$id];
}, $exludedTags),
]; ];
} }
public function create($path) public function create($path)
{ {
[$id, $subResource, $subResourceId] = $path; $id = $path[0] ?? null;
$subResource = $path[1] ?? null;
$subResourceId = $path[2] ?? null;
$id = $this->validateId($id); $id = $this->validateId($id);
$content = $this->decodeJSONContent(); $content = $this->decodeJSONContent();

View File

@ -75,6 +75,10 @@ class Guard
$check = $this->sql->prep('user_rememberme_check', 'SELECT user_id, region_data FROM mangadex_sessions WHERE session_token = ? AND created > UNIX_TIMESTAMP() - ?', $check = $this->sql->prep('user_rememberme_check', 'SELECT user_id, region_data FROM mangadex_sessions WHERE session_token = ? AND created > UNIX_TIMESTAMP() - ?',
[$rememberMeToken, SESSION_REMEMBERME_TIMEOUT], 'fetch', \PDO::FETCH_ASSOC, -1); [$rememberMeToken, SESSION_REMEMBERME_TIMEOUT], 'fetch', \PDO::FETCH_ASSOC, -1);
if (!$check) {
return;
}
$tokenRegionData = json_decode($check['region_data'], 1); $tokenRegionData = json_decode($check['region_data'], 1);
$userRegionData = $this->getClientDetails(); $userRegionData = $this->getClientDetails();
@ -142,7 +146,7 @@ class Guard
$sessionInfo['updated'] = time(); $sessionInfo['updated'] = time();
$sessionInfo['ip'] = _IP; $sessionInfo['ip'] = _IP;
$this->memcached->set('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT); $this->memcached->setSynced('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
setcookie(SESSION_COOKIE_NAME, setcookie(SESSION_COOKIE_NAME,
$sessionId, $sessionId,
time() + SESSION_TIMEOUT, time() + SESSION_TIMEOUT,
@ -169,7 +173,7 @@ class Guard
$sessionInfo['userid'] $sessionInfo['userid']
] ]
); );
$this->memcached->set("user_{$sessionInfo['userid']}_lastseen", 1, 60); $this->memcached->setSynced("user_{$sessionInfo['userid']}_lastseen", 1, 60);
} }
} else { } else {
// No session found? It could've been kicked out of memcached. Lets destroy the session cookie // No session found? It could've been kicked out of memcached. Lets destroy the session cookie
@ -196,7 +200,7 @@ class Guard
'userid' => (int)$userId, 'userid' => (int)$userId,
'is_rememberme' => $isRemembermeSession, 'is_rememberme' => $isRemembermeSession,
]; ];
$this->memcached->set('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT); $this->memcached->setSynced('session:'.$sessionId, $sessionInfo, time() + SESSION_TIMEOUT);
setcookie(SESSION_COOKIE_NAME, setcookie(SESSION_COOKIE_NAME,
$sessionId, $sessionId,
time() + SESSION_TIMEOUT, time() + SESSION_TIMEOUT,
@ -249,6 +253,9 @@ class Guard
public function verifyUserCredentials($userId, $rawPassword) public function verifyUserCredentials($userId, $rawPassword)
{ {
$user = $this->getUser($userId); $user = $this->getUser($userId);
if (!$user) {
return false;
}
return password_verify($rawPassword, $user->password); return password_verify($rawPassword, $user->password);
} }

View File

@ -5,22 +5,46 @@ namespace Mangadex\Model;
class MdexAtHomeClient class MdexAtHomeClient
{ {
public function getServerUrl(string $chapterHash, array $chapterPages, string $ip): string public function getServerUrl(string $chapterHash, array $chapterPages, string $ip, bool $onlySsl): string
{ {
$path = '/assign'; $path = '/assign';
$payload = [ $payload = [
'ip' => $ip, 'ip' => $ip,
'hash' => $chapterHash, 'hash' => $chapterHash,
'images' => $chapterPages, 'images' => $chapterPages,
'only_443' => $onlySsl,
]; ];
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload); $ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
return $this->queryAssign($ch, $payload);
}
public function getClientUrl(string $chapterHash, array $chapterPages, string $ip, string $clientId): string
{
$path = '/assign';
$payload = [
'ip' => $ip,
'hash' => $chapterHash,
'images' => $chapterPages,
'client_id' => $clientId,
];
$ch = $this->getCurl($path, $ip.$chapterHash.implode($chapterPages), $payload);
return $this->queryAssign($ch, $payload);
}
private function queryAssign($ch, $payload): string
{
$res = curl_exec($ch); $res = curl_exec($ch);
curl_close($ch);
if ($res === false) { if ($res === false) {
throw new \RuntimeException('MD@H::getServerUrl curl error: '.curl_error($ch)); $error = curl_error($ch);
curl_close($ch);
throw new \RuntimeException('MD@H::getServerUrl curl error: '.$error);
} }
curl_close($ch);
$dec = \json_decode($res, true); $dec = \json_decode($res, true);
if (!$dec) { if (!$dec) {
throw new \RuntimeException('MD@H::getServerUrl failed to decode: '.$res); throw new \RuntimeException('MD@H::getServerUrl failed to decode: '.$res);

View File

@ -13,7 +13,7 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover table-sm"> <table class="table table-striped table-hover table-sm">
<?php foreach ($templateVar['stats'] ?? [] AS $dsn => $serverStats) : ?> <?php foreach ($templateVar['stats'] ?? [] AS $dsn => $serverStats) : ?>
<?php if (empty($serverStats)) continue; ?> <?php if (empty($serverStats) || empty($serverStats[0] ?? [])) continue; ?>
<thead> <thead>
<tr class="border-top-0"> <tr class="border-top-0">
<th colspan="2">Server: <?=$dsn?></th> <th colspan="2">Server: <?=$dsn?></th>
@ -24,8 +24,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($serverStats as $key => $val) : ?> <?php foreach ($serverStats[0] as $key => $val) : ?>
<?php if (!in_array($key, ['Slave_IO_State', 'Slave_IO_Running', 'Seconds_Behind_Master'])) continue; ?> <?php if (!in_array($key, ['Relay_Log_File', 'Relay_Log_Space', 'Last_Error', 'Last_IO_Error', 'Last_SQL_Error', 'Slave_SQL_Running_State', 'Slave_IO_State', 'Slave_IO_Running', 'Seconds_Behind_Master'])) continue; ?>
<tr> <tr>
<td><?=$key?></td> <td><?=$key?></td>
<td><?=$val?></td> <td><?=$val?></td>

View File

@ -0,0 +1,27 @@
<!-- ad template -->
<?php
$banner = $templateVar[array_rand($templateVar)];
?>
<?php if (!isset($_COOKIE["hide_banner"])) : ?>
<div id="affiliate-banner" class="affiliate-banner mb-3">
<div class="d-flex position-relative">
<img id="close-banner" src="/images/banners/close.png" style="right:0;top:0;bottom:0;height:100%;cursor:pointer;" class="position-absolute"><a href="/support/affiliates">
<img class="w-100" src="/images/banners/affiliatebanner<?= $banner['banner_id'] ?>.<?= $banner['ext'] ?>">
</a>
</div>
<div class="d-none d-md-block" style="text-align: right;margin-right:20px;">
This affiliate banner was designed by <?= $banner["is_anonymous"] ? "an anonymous user" : display_user_link_v2($banner) ?>
</div>
<div class="d-md-none small" style="text-align: right;margin-right:20px;">
This affiliate banner was designed by <?= $banner["is_anonymous"] ? "an anonymous user" : display_user_link_v2($banner) ?>
</div>
</div>
<script type="text/javascript">
const closeButton = document.querySelector('#close-banner')
closeButton.addEventListener('click', evt => {
document.querySelector("#affiliate-banner").classList.add("d-none")
document.cookie = "hide_banner=true; max-age=2678400"
})
</script>
<?php endif; ?>

View File

@ -11,7 +11,7 @@
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<?= parse_template('ads/mobile_app_ad', $templateVar['banners']) ?>
<div class="card mb-3"> <div class="card mb-3">
<h6 class="card-header text-center"><?= display_fa_icon('external-link-alt') ?> <a href="/updates">Latest updates</a></h6> <h6 class="card-header text-center"><?= display_fa_icon('external-link-alt') ?> <a href="/updates">Latest updates</a></h6>
<div class="card-header p-0"> <div class="card-header p-0">

View File

@ -1,5 +1,9 @@
<?php
$env = (defined('DEBUG') && DEBUG) ? 'dev' : 'prod';
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
@ -18,29 +22,40 @@
<meta property="og:url" content="<?= "https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]" ?>" /> <meta property="og:url" content="<?= "https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]" ?>" />
<meta property="og:description" content="<?= $templateVar['og']['description'] ?>" /> <meta property="og:description" content="<?= $templateVar['og']['description'] ?>" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="twitter:site" content="@mangadex" /> <meta name="twitter:site" content="@mangadex" />
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png?1"> <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png?1">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png?1"> <link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png?1">
<link rel="manifest" href="/manifest.json" crossOrigin="use-credentials"> <link rel="manifest" href="/manifest.json" crossOrigin="use-credentials">
<link rel="search" type="application/opensearchdescription+xml" title="MangaDex Quick Search" href="/opensearch.xml"> <link rel="search" type="application/opensearchdescription+xml" title="MangaDex Quick Search" href="/opensearch.xml">
<?= $templateVar['og']['canonical'] ?> <?= $templateVar['og']['canonical'] ?>
<title><?= $templateVar['og']['title'] ?></title> <title><?= $templateVar['og']['title'] ?></title>
<!-- Google Tag Manager --> <!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': <script>
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], (function(w, d, s, l, i) {
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= w[l] = w[l] || [];
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); w[l].push({
})(window,document,'script','dataLayer','GTM-TS59XX9');</script> 'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src =
'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-TS59XX9');
</script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
<!-- Google fonts --> <!-- Google fonts -->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:regular,regularitalic,bold" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Ubuntu:regular,regularitalic,bold" rel="stylesheet">
<?php if ($templateVar['page'] == 'drama') { ?> <?php if ($templateVar['page'] == 'drama') { ?>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:regular,regularitalic,bold" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Open+Sans:regular,regularitalic,bold" rel="stylesheet">
<?php } ?> <?php } ?>
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link href="/bootstrap/css/bootstrap.css?<?= @filemtime(ABSPATH . '/bootstrap/css/bootstrap.css') ?>" rel="stylesheet" /> <link href="/bootstrap/css/bootstrap.css?<?= @filemtime(ABSPATH . '/bootstrap/css/bootstrap.css') ?>" rel="stylesheet" />
@ -50,7 +65,7 @@
<!-- OWL CSS --> <!-- OWL CSS -->
<link href="/scripts/owl/assets/owl.carousel.min.css" rel="stylesheet" /> <link href="/scripts/owl/assets/owl.carousel.min.css" rel="stylesheet" />
<link href="/scripts/owl/assets/owl.theme.default.min.css" rel="stylesheet" /> <link href="/scripts/owl/assets/owl.theme.default.min.css" rel="stylesheet" />
<!-- Fontawesone glyphicons --> <!-- Fontawesone glyphicons -->
<link href="/fontawesome/css/all.css" rel="stylesheet" /> <link href="/fontawesome/css/all.css" rel="stylesheet" />
@ -65,20 +80,16 @@
<?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?> <?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
<meta name="app" content="MangaDex" data-guest="<?= $templateVar['user']->user_id ? 0 : 1 ?>" data-chapter-id="<?= $_GET['id'] ?? '' ?>" data-page="<?= isset($_GET['p']) ? $_GET['p'] : 1 ?>" /> <meta name="app" content="MangaDex" data-guest="<?= $templateVar['user']->user_id ? 0 : 1 ?>" data-chapter-id="<?= $_GET['id'] ?? '' ?>" data-page="<?= isset($_GET['p']) ? $_GET['p'] : 1 ?>" />
<?php } <?php }
if (in_array($templateVar['page'], ['chapter']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?> if (in_array($templateVar['page'], ['chapter']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
<link href="/scripts/css/reader.css?<?= @filemtime(ABSPATH . "/scripts/css/reader.css") ?>" rel="stylesheet" /> <link href="/scripts/css/reader.css?<?= @filemtime(ABSPATH . "/scripts/css/reader.css") ?>" rel="stylesheet" />
<?php } ?> <?php } ?>
<?php if (defined('DEBUG') && DEBUG): ?> <script type="module" src="/dist/js/bundle.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.$env.js") ?>"></script>
<script type="module" src="/dist/js/bundle.dev.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.dev.js") ?>"></script>
<?php else: ?>
<script type="module" src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script>
<?php endif; ?>
</head> </head>
<body> <body>
<!-- Google Tag Manager (noscript) --> <!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TS59XX9" height="0" width="0" style="display:none; visibility:hidden"></iframe></noscript> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TS59XX9" height="0" width="0" style="display:none; visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) --> <!-- End Google Tag Manager (noscript) -->
<!-- Fixed navbar --> <!-- Fixed navbar -->
@ -88,27 +99,26 @@
<?php if (!$templateVar['user']->activated && $templateVar['user']->user_id) <?php if (!$templateVar['user']->activated && $templateVar['user']->user_id)
print display_alert("warning", "Warning", "Your account is currently unactivated. Please enter your activation code <a href='/activation'>here</a> for access to all of " . TITLE . "'s features."); ?> print display_alert("warning", "Warning", "Your account is currently unactivated. Please enter your activation code <a href='/activation'>here</a> for access to all of " . TITLE . "'s features."); ?>
<?php if (is_array($templateVar['announcement']) && count($templateVar['announcement']) > 0 && !$templateVar['user']->read_announcement && !in_array($templateVar['page'], ['chapter', 'list'])) { ?> <?php if (is_array($templateVar['announcement']) && count($templateVar['announcement']) > 0 && !$templateVar['user']->read_announcement && !in_array($templateVar['page'], ['chapter', 'list'])) { ?>
<div id="announcement" class="alert alert-success <?= $templateVar['user']->user_id ? 'alert-dismissible ' : ''?>fade show text-center" role="alert"> <div id="announcement" class="alert alert-success <?= $templateVar['user']->user_id ? 'alert-dismissible ' : '' ?>fade show text-center" role="alert">
<?php if ($templateVar['user']->user_id) { ?> <?php if ($templateVar['user']->user_id) { ?>
<button id="read_announcement_button" type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button id="read_announcement_button" type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<?php } ?> <?php } ?>
<?php foreach ($templateVar['announcement'] as $idx=>$row) { ?> <?php foreach ($templateVar['announcement'] as $idx => $row) { ?>
<strong>Announcement (<?= date('M-d', $row->timestamp) ?>):</strong> <?= $row->thread_name ?> <a title="Go to forum thread" href="/thread/<?= $row->thread_id ?>"><?= display_fa_icon('external-link-alt', 'Forum thread') ?></a> <strong>Announcement (<?= date('M-d', $row->timestamp) ?>):</strong> <?= $row->thread_name ?> <a title="Go to forum thread" href="/thread/<?= $row->thread_id ?>"><?= display_fa_icon('external-link-alt', 'Forum thread') ?></a>
<?php if($idx < count($templateVar['announcement']) - 1){ ?> <?php if ($idx < count($templateVar['announcement']) - 1) { ?>
<hr style="margin-right: -<?= $templateVar['user']->user_id ? 4 : 1.25 ?>rem; margin-left: -1.25rem;" /> <hr style="margin-right: -<?= $templateVar['user']->user_id ? 4 : 1.25 ?>rem; margin-left: -1.25rem;" />
<?php } ?> <?php } ?>
<?php } ?> <?php } ?>
</div> </div>
<?php } ?> <?php } ?>
<?php <?php
/** Print page content */ /** Print page content */
print $templateVar['page_html']; print $templateVar['page_html'];
if (validate_level($templateVar['user'], 'admin') && $templateVar['page'] != 'chapter') if (validate_level($templateVar['user'], 'admin') && $templateVar['page'] != 'chapter')
print_r ($templateVar['sql']->debug()); print_r($templateVar['sql']->debug());
?> ?>
</div> <!-- /container --> </div> <!-- /container -->
@ -122,7 +132,7 @@
<div class="modal-dialog modal-dialog-centered modal-lg" role="document"> <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="homepage_settings_label"><?= display_fa_icon('cog')?> MangaDex settings</h5> <h5 class="modal-title" id="homepage_settings_label"><?= display_fa_icon('cog') ?> MangaDex settings</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@ -160,24 +170,24 @@
</div> </div>
</div> </div>
<?php if ($templateVar['user']->hentai_mode) { ?> <?php if ($templateVar['user']->hentai_mode) { ?>
<div class="form-group row"> <div class="form-group row">
<label class="col-lg-3 col-form-label-modal" for="hentai_mode">Hentai:</label> <label class="col-lg-3 col-form-label-modal" for="hentai_mode">Hentai:</label>
<div class="col-lg-9"> <div class="col-lg-9">
<select class="form-control selectpicker show-tick" id="hentai_mode" name="hentai_mode"> <select class="form-control selectpicker show-tick" id="hentai_mode" name="hentai_mode">
<option value="0" <?= (!$templateVar['hentai_toggle']) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][0] ?>">Hide H</option> <option value="0" <?= (!$templateVar['hentai_toggle']) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][0] ?>">Hide H</option>
<option value="1" <?= ($templateVar['hentai_toggle'] == 1) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][1] ?>">All</option> <option value="1" <?= ($templateVar['hentai_toggle'] == 1) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][1] ?>">All</option>
<option value="2" <?= ($templateVar['hentai_toggle'] == 2) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][2] ?>">Only H</option> <option value="2" <?= ($templateVar['hentai_toggle'] == 2) ? 'selected' : '' ?> data-content="<?= $templateVar['hentai_options'][2] ?>">Only H</option>
</select> </select>
</div>
</div> </div>
</div>
<?php } ?> <?php } ?>
<div class="form-group row"> <div class="form-group row">
<div class="col-lg-3 text-right"> <div class="col-lg-3 text-right">
<button type="submit" class="btn btn-secondary" id="homepage_settings_button"><?= display_fa_icon('save') ?> Save</button> <button type="submit" class="btn btn-secondary" id="homepage_settings_button"><?= display_fa_icon('save') ?> Save</button>
</div> </div>
<div class="col-lg-9 text-left"> <div class="col-lg-9 text-left">
</div> </div>
</div> </div>
</form> </form>
</div> </div>
@ -189,14 +199,14 @@
<a class="btn btn-secondary mx-auto" role="button" href="/settings"><?= display_fa_icon('cog') ?> More settings</a> <a class="btn btn-secondary mx-auto" role="button" href="/settings"><?= display_fa_icon('cog') ?> More settings</a>
<?php } ?> <?php } ?>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<?= parse_template('partials/report_modal', $templateVar); ?> <?= parse_template('partials/report_modal', $templateVar); ?>
<footer class="footer"> <footer class="footer">
<p class="m-0 text-center text-muted">&copy; <?= date('Y') ?> <a href="/" title="<?php print_r($templateVar['memcached']->get($templateVar['ip'])) ?>">MangaDex</a> | <a href="https://path.net/" target="_blank" title="Provider of DDoS mitigation services">Path Network</a> | <a href="https://sdbx.moe/" target="_blank" title="seedbox provider">sdbx.moe</a></p> <p class="m-0 text-center text-muted">&copy; <?= date('Y') ?> <a href="/" title="<?php print_r($templateVar['memcached']->get($templateVar['ip'])) ?>">MangaDex</a> | <a href="https://path.net/" target="_blank" title="Provider of DDoS mitigation services">Path Network</a> | <a href="https://sdbx.moe/" target="_blank" title="seedbox provider">sdbx.moe</a> | <a href="https://ddos-guard.net?affiliate=119953" target="_blank" title="ddos-guard">DDoS Protection by DDoS-GUARD</a> | <a href="https://onramper.com/" target="_blank" title="Crypto Widget">Onramper</a></p>
</footer> </footer>
<?php <?php
@ -209,158 +219,161 @@
<!-- Bootstrap core JavaScript <!-- Bootstrap core JavaScript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- Placed at the end of the document so the pages load faster -->
<script nomodule src="/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>"></script> <script nomodule src="/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>"></script>
<script nomodule src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script> <script nomodule src="/dist/js/bundle.prod.js?<?= @filemtime(ABSPATH . "/dist/js/bundle.prod.js") ?>"></script>
<script src="/scripts/jquery.min.js?<?= @filemtime(ABSPATH . "/scripts/jquery.min.js") ?>"></script> <script src="/scripts/jquery.min.js?<?= @filemtime(ABSPATH . "/scripts/jquery.min.js") ?>"></script>
<script src="/scripts/jquery.touchSwipe.min.js"></script> <script src="/scripts/jquery.touchSwipe.min.js"></script>
<script src="/bootstrap/js/popper.min.js"></script> <script src="/bootstrap/js/popper.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js?1"></script> <script src="/bootstrap/js/bootstrap.min.js?1"></script>
<script src="/bootstrap/js/bootstrap-select.min.js?1"></script> <script src="/bootstrap/js/bootstrap-select.min.js?1"></script>
<script src="/scripts/lightbox2/js/lightbox.js"></script> <script src="/scripts/lightbox2/js/lightbox.js"></script>
<script src="/scripts/chart.min.js"></script> <script src="/scripts/chart.min.js"></script>
<?php if ($templateVar['page'] == 'home') { ?> <?php if ($templateVar['page'] == 'home') { ?>
<script src="/scripts/owl/owl.carousel.js"></script> <script src="/scripts/owl/owl.carousel.js"></script>
<?php } ?> <?php } ?>
<script> <script>
if (!('URL' in window) || !('URLSearchParams' in window)) { if (!('URL' in window) || !('URLSearchParams' in window)) {
document.head.appendChild(Object.assign(document.createElement("script"), { document.head.appendChild(Object.assign(document.createElement("script"), {
"src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>", "async": true, "src": "/dist/js/polyfills.prod.js?<?= @filemtime(ABSPATH . "/dist/js/polyfills.prod.js") ?>",
})) "async": true,
} }))
</script> }
<?php if (in_array($templateVar['page'], ['chapter', 'chapter_test']) && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?> </script>
<?php if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?>
<script src="/scripts/modernizr-custom.js"></script> <script src="/scripts/modernizr-custom.js"></script>
<?php } <script async src="/dist/js/reader.<?= $env ?>.js?<?= @filemtime(ABSPATH . "/dist/js/reader.$env.js") ?>"></script>
if ($templateVar['page'] == 'chapter' && (!isset($_GET['mode']) || $_GET['mode'] == 'chapter') && !$templateVar['user']->reader) { ?> <?php if ($env !== 'prod') { ?>
<script async src="/scripts/reader.min.js?<?= @filemtime(ABSPATH . "/scripts/reader.min.js") ?>"></script> <script nomodule src="/dist/js/reader.prod.js?<?= @filemtime(ABSPATH . "/dist/js/reader.prod.js") ?>"></script>
<?php } ?>
<?php } ?> <?php } ?>
<script src="/scripts/js/reporting.js"></script> <script src="/scripts/js/reporting.js"></script>
<script type="text/javascript"> <script type="text/javascript">
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?>
<?php if (defined('INCLUDE_JS_REDIRECT') && INCLUDE_JS_REDIRECT) : ?> var t = 'mang';
var t='mang'; t = t + 'adex.org';
t = t+'adex.org'; var w = 'www.mangadex.org';
var w='www.mangadex.org'; if (window.location.hostname != t && window.location.hostname != w) {
if (window.location.hostname != t && window.location.hostname != w ) { window.location = 'https://' + t;
window.location='https://'+t;
}
<?php endif; ?>
var $ = jQuery;
$(document).on('change', '.btn-file :file', function() {
var input = $(this),
numFiles = input.get(0).files ? input.get(0).files.length : 1,
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
input.trigger('fileselect', [numFiles, label]);
});
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function commaMultipleSelect(id) {
var list = document.getElementById(id);
var selected = new Array();
for (i = 0; i < list.options.length; i++) {
if (list.options[i].selected) {
selected.push(list.options[i].value);
} }
}
return selected.join(',');
}
function commaMultipleCheckbox(name) {
var list = document.getElementsByName(name);
var selected = new Array();
for (i = 0; i < list.length; i++) {
if (list[i].checked) {
selected.push(list[i].value);
}
}
return selected.join(',');
}
$(document).ready(function(){
var query = location.search;
<?php if (!isset($templateVar['user']->navigation) || $templateVar['user']->navigation) : ?>
$("#left_swipe_area").swipe({
swipeRight:function(event, direction, distance, duration, fingerCount) {
$('#left_modal').modal('toggle');
},
threshold: 50
});
$("#right_swipe_area").swipe({
swipeLeft:function(event, direction, distance, duration, fingerCount) {
$('#right_modal').modal('toggle');
},
threshold: 50
});
$("#right_modal").swipe({
swipeRight:function(event, direction, distance, duration, fingerCount) {
$('#right_modal').modal('toggle');
},
threshold: 50
});
$("#left_modal").swipe({
swipeLeft:function(event, direction, distance, duration, fingerCount) {
$('#left_modal').modal('toggle');
},
threshold: 50
});
<?php endif; ?> <?php endif; ?>
$("#read_announcement_button").click(function(event){ var $ = jQuery;
$.ajax({
url: "/ajax/actions.ajax.php?function=read_announcement", $(document).on('change', '.btn-file :file', function() {
type: 'GET', var input = $(this),
success: function (data) { numFiles = input.get(0).files ? input.get(0).files.length : 1,
$("#announcement").hide(); label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
}, input.trigger('fileselect', [numFiles, label]);
cache: false,
contentType: false,
processData: false
});
event.preventDefault();
}); });
$(".logout").click(function(event){ function capitalizeFirstLetter(string) {
$.ajax({ return string.charAt(0).toUpperCase() + string.slice(1);
type: "POST", }
url: "/ajax/actions.ajax.php?function=logout",
success: function(data) { function commaMultipleSelect(id) {
$("#message_container").html(data).show().delay(1500).fadeOut(); var list = document.getElementById(id);
location.reload(); var selected = new Array();
for (i = 0; i < list.options.length; i++) {
if (list.options[i].selected) {
selected.push(list.options[i].value);
} }
}
return selected.join(',');
}
function commaMultipleCheckbox(name) {
var list = document.getElementsByName(name);
var selected = new Array();
for (i = 0; i < list.length; i++) {
if (list[i].checked) {
selected.push(list[i].value);
}
}
return selected.join(',');
}
$(document).ready(function() {
var query = location.search;
<?php if (!isset($templateVar['user']->navigation) || $templateVar['user']->navigation) : ?>
$("#left_swipe_area").swipe({
swipeRight: function(event, direction, distance, duration, fingerCount) {
$('#left_modal').modal('toggle');
},
threshold: 50
});
$("#right_swipe_area").swipe({
swipeLeft: function(event, direction, distance, duration, fingerCount) {
$('#right_modal').modal('toggle');
},
threshold: 50
});
$("#right_modal").swipe({
swipeRight: function(event, direction, distance, duration, fingerCount) {
$('#right_modal').modal('toggle');
},
threshold: 50
});
$("#left_modal").swipe({
swipeLeft: function(event, direction, distance, duration, fingerCount) {
$('#left_modal').modal('toggle');
},
threshold: 50
});
<?php endif; ?>
$("#read_announcement_button").click(function(event) {
$.ajax({
url: "/ajax/actions.ajax.php?function=read_announcement",
type: 'GET',
success: function(data) {
$("#announcement").hide();
},
cache: false,
contentType: false,
processData: false
});
event.preventDefault();
}); });
event.preventDefault();
$(".logout").click(function(event) {
$.ajax({
type: "POST",
url: "/ajax/actions.ajax.php?function=logout",
success: function(data) {
$("#message_container").html(data).show().delay(1500).fadeOut();
location.reload();
}
});
event.preventDefault();
});
function highlightPost(node) {
if (node) {
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {
n.classList.remove('highlighted')
});
node.classList.add('highlighted')
}
}
if (location.hash)
highlightPost(document.querySelector(location.hash + ' .postbody'));
$('a.permalink').click(function(event) {
highlightPost(event.target.closest('.post').querySelector('.postbody'))
});
<?= $templateVar['page_scripts'] ?>
}); });
function highlightPost(node) {
if (node) {
Array.from(document.querySelectorAll('.highlighted')).forEach(function(n) {n.classList.remove('highlighted')});
node.classList.add('highlighted')
}
}
if (location.hash)
highlightPost(document.querySelector(location.hash + ' .postbody'));
$('a.permalink').click(function(event) {
highlightPost(event.target.closest('.post').querySelector('.postbody'))
});
<?= $templateVar['page_scripts'] ?>
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -29,7 +29,7 @@
switch ($templateVar['user']->reader_mode) { switch ($templateVar['user']->reader_mode) {
case 0: case 0:
?> ?>
<img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?? '' ?>" alt="image" data-page="<?= $templateVar['page'] ?>" /> <img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?? '' ?>" alt="image" data-page="<?= $templateVar['page'] ?>" />
@ -39,15 +39,13 @@
case 2: //long-strip case 2: //long-strip
foreach ($templateVar['page_array'] as $key => $x) { foreach ($templateVar['page_array'] as $key => $x) {
if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) { if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) {
?> ?>
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" /> <img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
<?php <?php
} } else {
?>
else {
?>
<img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" /> <img class="long-strip <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
<?php <?php
} }
} }
@ -56,17 +54,15 @@
case 3: //webtoon case 3: //webtoon
foreach ($templateVar['page_array'] as $key => $x) { foreach ($templateVar['page_array'] as $key => $x) {
if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) { if (!$templateVar['chapter']->server && $templateVar['chapter']->chapter_id == 256885 && in_array($key, [1])) {
?> ?>
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" /> <img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="/img.php?x=/data/<?= "{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
<?php <?php
} } else {
?>
else {
?>
<img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" /> <img class="webtoon <?= ($templateVar['user']->reader_click) ? "click" : "" ?>" src="<?= "{$templateVar['server']}{$templateVar['chapter']->chapter_hash}/$x" ?>" alt="image <?= $key ?>" />
<?php <?php
} }
} }
@ -75,7 +71,7 @@
default: default:
?> ?>
<img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?>" alt="image" data-page="<?= $templateVar['page'] ?>" /> <img id="current_page" class="reader <?= ($templateVar['user']->image_fit == 2) ? 'max-height' : 'max-width' ?>" src="<?= $templateVar['server'] ?><?= $templateVar['chapter']->chapter_hash ?>/<?= $templateVar['page_array'][$templateVar['page']] ?>" alt="image" data-page="<?= $templateVar['page'] ?>" />
<?php <?php
break; break;
} }
?> ?>
@ -111,7 +107,7 @@
<div class="modal-dialog modal-dialog-centered modal-lg" role="document"> <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="report_chapter_label"><?= display_fa_icon('cog')?> Report chapter</h5> <h5 class="modal-title" id="report_chapter_label"><?= display_fa_icon('cog') ?> Report chapter</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@ -123,9 +119,11 @@
<div class="col-md-9"> <div class="col-md-9">
<select required title="Select a reason" class="form-control selectpicker" id="type_id" name="type_id"> <select required title="Select a reason" class="form-control selectpicker" id="type_id" name="type_id">
<?php <?php
$chapter_reasons = array_filter($templateVar['report_reasons'], function($reason) { return REPORT_TYPES[$reason['type_id']] === 'Chapter'; }); $chapter_reasons = array_filter($templateVar['report_reasons'], function ($reason) {
foreach ($chapter_reasons as $reason): ?> return REPORT_TYPES[$reason['type_id']] === 'Chapter';
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option> });
foreach ($chapter_reasons as $reason) : ?>
<option value="<?= $reason['id'] ?>"><?= $reason['text'] ?><?= $reason['is_info_required'] ? ' *' : '' ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
@ -133,7 +131,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="chapter_name" class="col-md-3 col-form-label">Explanation</label> <label for="chapter_name" class="col-md-3 col-form-label">Explanation</label>
<div class="col-md-9"> <div class="col-md-9">
<textarea class="form-control" id="info" name="info" placeholder="Optional" ></textarea> <textarea class="form-control" id="info" name="info" placeholder="Optional"></textarea>
</div> </div>
</div> </div>
<div class="text-center"> <div class="text-center">
@ -150,7 +148,7 @@
<div class="modal-dialog modal-dialog-centered modal-lg" role="document"> <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="legacy_reader_settings_label"><?= display_fa_icon('cog')?> Legacy reader settings</h5> <h5 class="modal-title" id="legacy_reader_settings_label"><?= display_fa_icon('cog') ?> Legacy reader settings</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@ -176,6 +174,15 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="data_saver" class="col-md-3 col-form-label">Data saver:</label>
<div class="col-md-9">
<select class="form-control selectpicker" id="data_saver" name="data_saver">
<option <?= !$templateVar['user']->data_saver ? 'selected' : '' ?> value="0">Off</option>
<option <?= $templateVar['user']->data_saver ? 'selected' : '' ?> value="1">On</option>
</select>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="reader_mode" class="col-md-3 col-form-label">Reader mode:</label> <label for="reader_mode" class="col-md-3 col-form-label">Reader mode:</label>
<div class="col-md-9"> <div class="col-md-9">

View File

@ -187,6 +187,7 @@ $links_array = ($templateVar['manga']->manga_links) ? json_decode($templateVar['
<div class="col-lg-3 col-xl-2 strong">Mod:</div> <div class="col-lg-3 col-xl-2 strong">Mod:</div>
<div class="col-lg-9 col-xl-10"> <div class="col-lg-9 col-xl-10">
<?= display_lock_manga($templateVar['user'], $templateVar['manga']) ?> <?= display_lock_manga($templateVar['user'], $templateVar['manga']) ?>
<?= display_regenerate_manga_thumb($templateVar['user']) ?>
<?= display_delete_manga($templateVar['user']) ?> <?= display_delete_manga($templateVar['user']) ?>
</div> </div>
</div> </div>

View File

@ -4,7 +4,6 @@
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'stats') ? 'active' : '' ?>" href="/md_at_home/stats"><?= display_fa_icon('chart-line', 'Statistics') ?> <span class="d-none d-lg-inline">Statistics</span></a></li> <li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'stats') ? 'active' : '' ?>" href="/md_at_home/stats"><?= display_fa_icon('chart-line', 'Statistics') ?> <span class="d-none d-lg-inline">Statistics</span></a></li>
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'request') ? 'active' : '' ?>" href="/md_at_home/request"><?= display_fa_icon('envelope', 'Request a client') ?> <span class="d-none d-lg-inline">Request a client</span></a></li> <li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'request') ? 'active' : '' ?>" href="/md_at_home/request"><?= display_fa_icon('envelope', 'Request a client') ?> <span class="d-none d-lg-inline">Request a client</span></a></li>
<?php if (validate_level($templateVar['user'], 'member')) : ?> <?php if (validate_level($templateVar['user'], 'member')) : ?>
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'options') ? 'active' : '' ?>" href="/md_at_home/options"><?= display_fa_icon('cog', 'Options') ?> <span class="d-none d-lg-inline">Options</span></a></li>
<li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'clients') ? 'active' : '' ?>" href="/md_at_home/clients"><?= display_fa_icon('server', 'My clients') ?> <span class="d-none d-lg-inline">My clients</span></a></li> <li class="nav-item"><a class="nav-link <?= ($templateVar['section'] == 'clients') ? 'active' : '' ?>" href="/md_at_home/clients"><?= display_fa_icon('server', 'My clients') ?> <span class="d-none d-lg-inline">My clients</span></a></li>
<?php endif; ?> <?php endif; ?>
<?php if (validate_level($templateVar['user'], 'admin')) : ?> <?php if (validate_level($templateVar['user'], 'admin')) : ?>

View File

@ -1,19 +1,18 @@
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th><?= display_fa_icon('hashtag') ?></th> <th class="text-center"><?= display_fa_icon('hashtag') ?></th>
<th><?= display_fa_icon('user') ?></th> <th><?= display_fa_icon('user') ?></th>
<th>IP</th> <th>IP</th>
<th><?= display_fa_icon('globe-asia') ?></th> <th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
<th><?= display_fa_icon('globe') ?></th> <th class="text-center"><?= display_fa_icon('globe-asia') ?></th>
<th><?= display_fa_icon('bolt') ?></th> <th class="text-center"><?= display_fa_icon('globe') ?></th>
<th><?= display_fa_icon('upload', 'Mbps') ?></th> <th class="text-center"><?= display_fa_icon('bolt') ?></th>
<th><?= display_fa_icon('download', 'Mbps') ?></th> <th class="text-center"><?= display_fa_icon('upload', 'Mbps') ?></th>
<th><?= display_fa_icon('hdd', 'GB', '', 'far') ?></th> <th class="text-center"><?= display_fa_icon('download', 'Mbps') ?></th>
<th class="text-center"><?= display_fa_icon('hdd', 'GB', '', 'far') ?></th>
<th><?= display_fa_icon('calendar-alt') ?></th> <th><?= display_fa_icon('calendar-alt') ?></th>
<th><?= display_fa_icon('key') ?></th> <th><?= display_fa_icon('key') ?></th>
<th>Data transferred (GB)</th>
<th>Daily average (GB)</th>
<th><?= display_fa_icon('check') ?><?= display_fa_icon('times') ?></th> <th><?= display_fa_icon('check') ?><?= display_fa_icon('times') ?></th>
</tr> </tr>
</thead> </thead>
@ -23,7 +22,8 @@
<td>#<?= $client_id ?></td> <td>#<?= $client_id ?></td>
<td><?= display_user_link($client->user_id, $client->username, $client->level_colour) ?></td> <td><?= display_user_link($client->user_id, $client->username, $client->level_colour) ?></td>
<td><?= $client->client_ip ?></td> <td><?= $client->client_ip ?></td>
<td><?= $client->client_continent ?></td> <td class="text-center"><?= $client->approved === 1 ? "<a target='_blank' href='{$templateVar['backend']->getClientUrl('a61fa9f7f1313194787116d1357a7784', ['N9.jpg'], $templateVar['ip'], $client_id)}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td>
<td><?= $client->client_continent ?></td>
<td><img src="/images/flags/<?= $client->client_country ?>.png" alt="<?= $client->client_country ?>" /></td> <td><img src="/images/flags/<?= $client->client_country ?>.png" alt="<?= $client->client_country ?>" /></td>
<td><a href="<?= $client->speedtest ?>" target="_blank"><?= display_fa_icon('external-link-alt') ?></a></td> <td><a href="<?= $client->speedtest ?>" target="_blank"><?= display_fa_icon('external-link-alt') ?></a></td>
<td><?= $client->upload_speed ?></td> <td><?= $client->upload_speed ?></td>
@ -31,8 +31,6 @@
<td><?= $client->disk_cache_size ?></td> <td><?= $client->disk_cache_size ?></td>
<td><?= $client->timestamp ? date('Y-m-d H:i:s', $client->timestamp) . ' UTC' : '' ?></td> <td><?= $client->timestamp ? date('Y-m-d H:i:s', $client->timestamp) . ' UTC' : '' ?></td>
<td><code><?= $client->client_secret ?></code></td> <td><code><?= $client->client_secret ?></code></td>
<td>0</td>
<td>0</td>
<td> <td>
<?php if ($client->approved === 0 || $client->approved === NULL) : ?> <?php if ($client->approved === 0 || $client->approved === NULL) : ?>
<button class="btn btn-success btn-sm approve_button" data-id="<?= $client_id ?>"><?= display_fa_icon('check') ?></button> <button class="btn btn-success btn-sm approve_button" data-id="<?= $client_id ?>"><?= display_fa_icon('check') ?></button>
@ -60,14 +58,13 @@
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
<th><?= $total_upload ?></th> <th><?= $total_upload ?></th>
<th><?= $total_download ?></th> <th><?= $total_download ?></th>
<th><?= $total_disk ?></th> <th><?= $total_disk ?></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th>total</th>
<th>avg</th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -2,19 +2,14 @@
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th><?= display_fa_icon('hashtag', 'Client ID') ?></th> <th class="text-center"><?= display_fa_icon('hashtag', 'Client ID') ?></th>
<th>IP</th> <th>IP</th>
<th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th> <th class="text-center"><?= display_fa_icon('network-wired', 'Test') ?></th>
<th class="text-center"><?= display_fa_icon('globe-asia', 'Continent') ?></th> <th class="text-center"><?= display_fa_icon('globe-asia', 'Continent') ?></th>
<th class="text-center"><?= display_fa_icon('globe', 'Country') ?></th> <th class="text-center"><?= display_fa_icon('globe', 'Country') ?></th>
<th class="text-center"><?= display_fa_icon('upload', 'Upload speed (Mbps)') ?></th>
<th class="text-center"><?= display_fa_icon('download', 'Download speed (Mbps)') ?></th>
<th class="text-center"><?= display_fa_icon('hdd', 'Disk allocation (GB)') ?></th>
<th class="text-center">Status</th> <th class="text-center">Status</th>
<th><?= display_fa_icon('calendar-alt', 'Time of approval') ?></th> <th><?= display_fa_icon('calendar-alt', 'Time of approval') ?></th>
<th><?= display_fa_icon('key', 'Client secret') ?></th> <th><?= display_fa_icon('key', 'Client secret') ?></th>
<!--<th>Data transferred (GB)</th>
<th>Daily average (GB)</th>-->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -27,72 +22,31 @@
<tr class="text-<?= $client['approved'] ? 'success' : ($client['approved'] === 0 ? 'danger' : 'warning' ) ?>"> <tr class="text-<?= $client['approved'] ? 'success' : ($client['approved'] === 0 ? 'danger' : 'warning' ) ?>">
<td>#<?= $client['client_id'] ?></td> <td>#<?= $client['client_id'] ?></td>
<td><?= $client['client_ip'] ?></td> <td><?= $client['client_ip'] ?></td>
<td class="text-center"><?= $client['approved'] ? "<a target='_blank' href='https://{$client['client_subsubdomain']}.mangadex.network:{$client['client_port']}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td> <td class="text-center"><?= $client['approved'] ? "<a target='_blank' href='{$templateVar['backend']->getClientUrl('a61fa9f7f1313194787116d1357a7784', ['N9.jpg'], $templateVar['ip'], $client['client_id'])}/data/a61fa9f7f1313194787116d1357a7784/N9.jpg' class='btn btn-sm btn-info' title='Test your client'>" . display_fa_icon('network-wired', 'Test') . "</a>" : "<button class='btn btn-sm btn-info disabled'>" . display_fa_icon('network-wired', 'Test') . "</button>"?></td>
<td class="text-center"><?= $client['client_continent'] ?></td> <td class="text-center"><?= $client['client_continent'] ?></td>
<td class="text-center"><img src="/images/flags/<?= $client['client_country'] ?>.png" alt="<?= $client['client_country'] ?>" title="<?= $client['client_country'] ?>" /></td> <td class="text-center"><img src="/images/flags/<?= $client['client_country'] ?>.png" alt="<?= $client['client_country'] ?>" title="<?= $client['client_country'] ?>" /></td>
<td class="text-center"><?= $client['upload_speed'] ?></td>
<td class="text-center"><?= $client['download_speed'] ?></td>
<td class="text-center"><?= $client['disk_cache_size'] ?></td>
<td class="text-center"><?= $client['approved'] ? display_fa_icon('check', 'Approved') : ($client['approved'] === 0 ? display_fa_icon('times', 'rejected') : 'Pending' ) ?></td> <td class="text-center"><?= $client['approved'] ? display_fa_icon('check', 'Approved') : ($client['approved'] === 0 ? display_fa_icon('times', 'rejected') : 'Pending' ) ?></td>
<td><?= $client['timestamp'] ? date('Y-m-d H:i:s', $client['timestamp']) . ' UTC' : '' ?></td> <td><?= $client['timestamp'] ? date('Y-m-d H:i:s', $client['timestamp']) . ' UTC' : '' ?></td>
<td><code><?= $client['client_secret'] ?></code></td> <td><code><?= $client['client_secret'] ?></code></td>
<!--<td>0</td> <!--<td>0</td>
<td>0</td>--> <td>0</td>-->
</tr> </tr>
<?php
$total_upload += $client['upload_speed'];
$total_download += $client['download_speed'];
$total_disk += $client['disk_cache_size'];
?>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th><?= $total_upload ?></th>
<th><?= $total_download ?></th>
<th><?= $total_disk ?></th>
<th></th>
<th></th>
<th></th>
<!--<th>total</th>
<th>avg</th>-->
</tr>
</tfoot>
</table> </table>
<h3>Instructions: </h3> <h3>Instructions: </h3>
<?php if ($templateVar['approvaltime']) : ?> <?php if ($templateVar['approvaltime']) : ?>
<ul> <ul>
<li>Download the <a href="/dl/mangadex_at_home-1.2.2.zip">client</a> and included settings sample (right click and save as).</li> <li>Download <a href="/dl/mangadex_at_home-2.0.0.zip">the client and settings file</a>.</li>
<pre class="bg-dark text-light p-2">md5: 7c0c8941544ec09f637a4e8e49204d96 <pre class="bg-dark text-light p-2">md5: e077e54df77d406d973ce269a7e7febb
sha-256: 68e26adf68268ae9781919fd5dd80a29595eabf562bbc0cda3dcbe332dd959d8</pre> sha-256: ef4d139b346837b223c15032dd9f38790fe77d7b5f40320f1d82f6caf7a7bb10</pre>
<li>settings.json needs editing to your config.</li> <li>settings.sample.yaml needs editing to your config, and renamed as settings.yaml</li>
<pre class="bg-dark text-light p-2">settings.json example:
{
"client_secret": "iiesenpaithisisoursecret",
"client_hostname": "0.0.0.0", // "0.0.0.0" is the default and binds to everything
"client_port": 443, // 443 is recommended if possible, otherwise use something higher, e.g. 44300
"client_external_port": 0, //443 is recommended; This port will be send to mdah-backend.
//You need to forward this to the client_port in your router - 0 uses `client_port`
"threads": 16,
"graceful_shutdown_wait_seconds": 60, // Time from graceful shutdown start to force quit
// This rounds down to 15-second increments
"max_cache_size_in_mebibytes": 80000,
"max_kilobits_per_second": 0, // 0 disables max brust limiting
"max_mebibytes_per_hour": 0, // 0 disables hourly bandwidth limiting
"web_settings": { // delete this block to disable webui
"ui_hostname": "127.0.0.1", // "127.0.0.1" is the default and binds to localhost only
"ui_port": 8080
}
}</pre>
<li>To start the client, put .jar and settings.json in same folder and run: </li> <li>To start the client, put .jar and settings.json in same folder and run: </li>
<pre class="bg-dark text-light p-2">java -Dfile-level=trace -Dstdout-level=info -jar mangadex_at_home-1.2.2-all.jar</pre> <pre class="bg-dark text-light p-2">java -Dfile-level=trace -Dstdout-level=info -jar mangadex_at_home-2.0.0-all.jar</pre>
<li>For no logging, run: </li> <li>For no logging, run: </li>
<pre class="bg-dark text-light p-2">java -Dfile-level=off -Dstdout-level=info -jar mangadex_at_home-1.2.2-all.jar</pre> <pre class="bg-dark text-light p-2">java -Dfile-level=off -Dstdout-level=info -jar mangadex_at_home-2.0.0-all.jar</pre>
<li>(The name of the .jar file will change in future versions, so edit accordingly.)</li> <li>(The name of the .jar file will change in future versions, so edit accordingly.)</li>
<li>If you need help, come on <a href="https://discord.gg/mangadex">Discord</a>.</li> <li>If you need help, come on <a href="https://discord.gg/mangadex">Discord</a>.</li>
</ul> </ul>

View File

@ -7,7 +7,7 @@
<ul> <ul>
<li>An IPv4 address (static or dynamic).</li> <li>An IPv4 address (static or dynamic).</li>
<li>A minimum network speed of 80Mbps up/down.</li> <li>A minimum network speed of 40Mbps up/down.</li>
<li>A minimum of 40GB of dedicated storage space, preferably more.</li> <li>A minimum of 40GB of dedicated storage space, preferably more.</li>
<li>24/7 availability (This means the machine must be *on* 24/7).</li> <li>24/7 availability (This means the machine must be *on* 24/7).</li>
</ul> </ul>

Some files were not shown because too many files have changed in this diff Show More