Plugin Directory

Changeset 3266307


Ignore:
Timestamp:
04/03/2025 11:44:59 AM (12 months ago)
Author:
quentn
Message:

Fixed SQL injection vulnerabilities

Location:
quentn-wp/trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • quentn-wp/trunk/includes/class-quentn-wp-access-overview-list.php

    r3073163 r3266307  
    215215    public function get_quentn_restrictions( $per_page = 5, $page_number = 1, $page_id ) {
    216216        global $wpdb;
    217         $sql = "SELECT * FROM " . $wpdb->prefix . TABLE_QUENTN_RESTRICTIONS. " where page_id='". $page_id."'";
    218         //set search
    219         if ( ! empty( $_REQUEST['s'] ) ) {
    220             $sql .= " and email LIKE '%" . esc_sql( $_REQUEST['s'] )."%'";
     217
     218        $sql = $wpdb->prepare("SELECT * FROM {$wpdb->prefix}" . TABLE_QUENTN_RESTRICTIONS . " WHERE page_id = %d", $page_id );
     219
     220        if (!empty($_REQUEST['s'])) {
     221            $search = '%' . $wpdb->esc_like($_REQUEST['s']) . '%';
     222            $sql .= $wpdb->prepare(" AND email LIKE %s", $search);
    221223        }
    222224
     
    298300    public function record_count($page_id) {
    299301        global $wpdb;
    300         $sql = "SELECT COUNT(*) FROM ".$wpdb->prefix . TABLE_QUENTN_RESTRICTIONS. " where page_id='".$page_id."'";
    301         if ( ! empty( $_REQUEST['s'] ) ) {
    302             $sql .= " and email LIKE '%". esc_sql( $_REQUEST['s'] )."%'";
     302        $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}" . TABLE_QUENTN_RESTRICTIONS . "  WHERE page_id = %d", $page_id );
     303        if (!empty($_REQUEST['s'])) {
     304            $search = '%' . $wpdb->esc_like($_REQUEST['s']) . '%';
     305            $sql .= $wpdb->prepare(" AND email LIKE %s", $search);
    303306        }
    304307
     
    387390            if( ! empty( $delete_restrict_pages_ids ) ) {
    388391                //delete multiple accesses
    389                 $query =  "DELETE FROM ".$wpdb->prefix . TABLE_QUENTN_RESTRICTIONS." where CONCAT_WS('|', page_id, email) IN ('".implode("', '", $delete_restrict_pages_ids)."')";
     392                $placeholders = implode(',', array_fill(0, count($delete_restrict_pages_ids), '%s'));
     393                $query = $wpdb->prepare(
     394                    "DELETE FROM {$wpdb->prefix}" . TABLE_QUENTN_RESTRICTIONS . "
     395                          WHERE CONCAT_WS('|', page_id, email) IN ($placeholders)",
     396                          $delete_restrict_pages_ids
     397                );
    390398                //$wpdb->query( $wpdb->query( $query ) );
    391399                $num_records_deleted = $wpdb->query( $query );
  • quentn-wp/trunk/includes/class-quentn-wp-rest-api-controller.php

    r3073163 r3266307  
    1616    private $namespace;
    1717
    18     /**
     18    /**
    1919     * The first URL segment after core prefix
    2020     *
     
    7979    private $get_tracking;
    8080
    81     /**
     81    /**
    8282     * The base URL for route to get quentn logs
    8383     *
     
    8888    private $get_logs;
    8989
    90     /**
     90    /**
    9191     * The base URL for route to get page access
    9292     *
     
    9797    private $get_page_access;
    9898
    99     /**
     99    /**
    100100     * The base URL for route to get a page restriction settings
    101101     *
     
    220220        ));
    221221
    222         //register route to get list of all pages having quentn restrictions active
     222        //register route to get list of all pages having quentn restrictions active
    223223        register_rest_route( $this->namespace_v2, $this->get_page_restrictions, array(
    224224            // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
     
    308308        ));
    309309
    310         //register route to get logs
     310        //register route to get logs
    311311        register_rest_route( $this->namespace, $this->get_logs, array(
    312312            // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
     
    329329        ));
    330330
    331         //register route to get page access
    332         register_rest_route( $this->namespace, $this->get_page_access, array(
    333             // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
    334             'methods' => \WP_REST_Server::CREATABLE,
    335             // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
    336             'callback' => array( $this, 'quentn_get_page_access' ),
    337             // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
    338             'permission_callback' => array( $this, 'quentn_check_credentials' ),
    339 
    340             'args' => array(
    341                 'data' => array(
    342                     'required' => true,
    343                     'type' => 'string',
    344                 ),
    345                 'vu' => array(
    346                     'required' => true,
    347                     'type' => 'integer',
    348                 ),
    349             ),
    350         ));
    351 
    352         //register route to get page restriction settings
    353         register_rest_route( $this->namespace, $this->page_restriction_settings, array(
    354             // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
    355             'methods' => \WP_REST_Server::CREATABLE,
    356             // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
    357             'callback' => array( $this, 'quentn_get_page_restriction_settings' ),
    358             // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
    359             'permission_callback' => array( $this, 'quentn_check_credentials' ),
    360 
    361             'args' => array(
    362                 'data' => array(
    363                     'required' => true,
    364                     'type' => 'string',
    365                 ),
    366                 'vu' => array(
    367                     'required' => true,
    368                     'type' => 'integer',
    369                 ),
    370             ),
    371         ));
     331        //register route to get page access
     332        register_rest_route( $this->namespace, $this->get_page_access, array(
     333            // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
     334            'methods' => \WP_REST_Server::CREATABLE,
     335            // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
     336            'callback' => array( $this, 'quentn_get_page_access' ),
     337            // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
     338            'permission_callback' => array( $this, 'quentn_check_credentials' ),
     339
     340            'args' => array(
     341                'data' => array(
     342                    'required' => true,
     343                    'type' => 'string',
     344                ),
     345                'vu' => array(
     346                    'required' => true,
     347                    'type' => 'integer',
     348                ),
     349            ),
     350        ));
     351
     352        //register route to get page restriction settings
     353        register_rest_route( $this->namespace, $this->page_restriction_settings, array(
     354            // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
     355            'methods' => \WP_REST_Server::CREATABLE,
     356            // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
     357            'callback' => array( $this, 'quentn_get_page_restriction_settings' ),
     358            // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
     359            'permission_callback' => array( $this, 'quentn_check_credentials' ),
     360
     361            'args' => array(
     362                'data' => array(
     363                    'required' => true,
     364                    'type' => 'string',
     365                ),
     366                'vu' => array(
     367                    'required' => true,
     368                    'type' => 'integer',
     369                ),
     370            ),
     371        ));
    372372    }
    373373
     
    382382    public function quentn_grant_access( $request ) {
    383383        global $wpdb;
     384
    384385        $request_body = json_decode( $request->get_body(), true );
    385         //decode and save request in array
    386         $quentn_page_timer_permission = json_decode( base64_decode( $request_body['data'] ), true );
    387         $emails = $quentn_page_timer_permission['data']['email'] ?? array();
    388         $pages = $quentn_page_timer_permission['data']['page'] ?? array();
    389 
    390         //set values and place holders to insert into database in one query
    391         $values = array();
    392         $place_holders = array();
     386        $quentn_data = json_decode( base64_decode( $request_body['data'] ), true );
     387
     388        $emails = isset( $quentn_data['data']['email'] ) ? $quentn_data['data']['email'] : array();
     389        $pages = isset( $quentn_data['data']['page'] ) ? $quentn_data['data']['page'] : array();
     390
     391        $placeholders = [];
     392        $values = [];
     393        $now = time();
     394
    393395        foreach ( $emails as $email ) {
    394             if ( $email == "" ) { //email cannot be empty
     396            $email = sanitize_email( $email );
     397            if ( empty( $email ) ) {
    395398                continue;
    396399            }
     400
     401            $email_hash = hash( 'sha256', $email );
     402
    397403            foreach ( $pages as $page ) {
    398                 array_push( $values, $page, $email, hash( 'sha256', $email ), time() );
    399                 $place_holders[] = "('%d', '%s', '%s', '%d')";
    400             }
    401         }
    402 
    403         //insert into database
    404         $query = "INSERT INTO ".$wpdb->prefix . TABLE_QUENTN_RESTRICTIONS." ( page_id, email, email_hash, created_at ) VALUES ";
    405         $query .= implode( ', ', $place_holders );
    406         $wpdb->query( $wpdb->prepare( "$query ON DUPLICATE KEY UPDATE created_at= ".time(), $values ) );
    407         do_action( 'quentn_access_granted', $emails, $pages, QUENTN_WP_ACCESS_ADDED_BY_API );
     404                $page = intval( $page );
     405                $placeholders[] = "(%d, %s, %s, %d)";
     406                array_push( $values, $page, $email, $email_hash, $now );
     407            }
     408        }
     409
     410        if ( empty( $placeholders ) ) {
     411            return new WP_Error( 'no_values', __( 'No valid values to grant access.', 'quentn-wp' ), array( 'status' => 400 ) );
     412        }
     413
     414        $table = $wpdb->prefix . TABLE_QUENTN_RESTRICTIONS;
     415        $sql = "INSERT INTO $table (page_id, email, email_hash, created_at) VALUES ";
     416        $sql .= implode( ', ', $placeholders );
     417        $sql .= " ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)";
     418
     419        $prepared = $wpdb->prepare( $sql, $values );
     420        $wpdb->query( $prepared );
     421
     422        do_action( 'quentn_access_granted', $emails, $pages, QUENTN_WP_ACCESS_ADDED_BY_API );
     423
    408424        return rest_ensure_response( esc_html__( 'Permissions Timer Successfully Updated', 'quentn-wp' ) );
    409425    }
     
    421437
    422438        $request_body = json_decode( $request->get_body(), true );
    423 
    424         $quentn_page_timer_permission = json_decode ( base64_decode( $request_body['data'] ), true );
    425 
    426         $emails = isset( $quentn_page_timer_permission['data']['email'] ) ? $quentn_page_timer_permission['data']['email'] : array();
    427         $pages = isset( $quentn_page_timer_permission['data']['page'] ) ? $quentn_page_timer_permission['data']['page'] : array();
    428 
    429         //set values and place holders to insert into database in one query
    430         $values = array();
     439        $quentn_data = json_decode( base64_decode( $request_body['data'] ), true );
     440
     441        $emails = isset( $quentn_data['data']['email'] ) ? $quentn_data['data']['email'] : array();
     442        $pages = isset( $quentn_data['data']['page'] ) ? $quentn_data['data']['page'] : array();
     443
     444        // Collect safe page-email pairs
     445        $conditions = [];
     446        $values     = [];
     447
    431448        foreach ( $emails as $email ) {
     449            $email = sanitize_email( $email );
    432450            foreach ( $pages as $page ) {
    433                 $pageid_email = trim( $page )."|".trim( $email );
    434                 array_push( $values, $pageid_email );
    435             }
    436         }
    437 
    438         //delete permissions
    439         $query =  "DELETE FROM ".$wpdb->prefix . TABLE_QUENTN_RESTRICTIONS." where CONCAT_WS('|', page_id, email) IN ('".implode("','", $values)."')";
    440         $affected_rows = $wpdb->query( $query );
    441         if ( $affected_rows ) {
    442             do_action( 'quentn_access_revoked', $emails, $pages, QUENTN_WP_ACCESS_REVOKED_BY_API );
    443         }
     451                $page = intval( $page );
     452                $conditions[] = "(page_id = %d AND email = %s)";
     453                $values[] = $page;
     454                $values[] = $email;
     455            }
     456        }
     457
     458        if ( empty( $conditions ) ) {
     459            return new WP_Error( 'no_values', __( 'No valid values to revoke access.', 'quentn-wp' ), array( 'status' => 400 ) );
     460        }
     461
     462        // Build secure query
     463        $where = implode( ' OR ', $conditions );
     464        $query = "DELETE FROM {$wpdb->prefix}" . TABLE_QUENTN_RESTRICTIONS . " WHERE $where";
     465
     466        $prepared_query = $wpdb->prepare( $query, $values );
     467        $affected_rows = $wpdb->query( $prepared_query );
     468
     469        if ( $affected_rows ) {
     470            do_action( 'quentn_access_revoked', $emails, $pages, QUENTN_WP_ACCESS_REVOKED_BY_API );
     471        }
    444472
    445473        return rest_ensure_response( esc_html__( 'Permissions Timer Successfully Updated', 'quentn-wp' ) );
     
    461489            if( get_post_meta( $id->ID, '_quentn_post_restrict_meta', true ) ) {
    462490                $restricted_pages[] = array(
    463                                             "page_id"    => $id->ID,
    464                                             "page_title" => $id->post_title,
    465                                         );
    466             }
    467         }
    468         //todo remove json_encode function
     491                    "page_id"    => $id->ID,
     492                    "page_title" => $id->post_title,
     493                );
     494            }
     495        }
     496        //todo remove json_encode function
    469497        return rest_ensure_response( json_encode( $restricted_pages ) );
    470498    }
    471499
    472     /**
     500    /**
    473501     * Get list of all pages where quentn restrictions are applied
    474502     *
    475503     * @since  1.2.8
    476504     * @access public
    477     * @param WP_REST_Request $request The current request object.
    478     * @return WP_Error|WP_REST_Response
     505    * @param WP_REST_Request $request The current request object.
     506    * @return WP_Error|WP_REST_Response
    479507     */
    480508    public function quentn_get_restricted_pages_v2( $request ) {
    481         $request_body = json_decode( $request->get_body(), true );
    482         $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
    483         $request_data = $request_body_data['data'];
    484         $args = array(
    485             'post_type' => 'page',
    486             'meta_key' => '_quentn_post_restrict_meta'
    487         );
    488 
    489         $limit = ! empty( $request_data['limit'] ) ? $request_data['limit'] : 50; // get all posts if not mentioned
    490         $args['posts_per_page'] = $limit;
    491 
    492         if ( ! empty( $request_data['order_by'] ) ) {
    493             $args['orderby'] = $request_data['order_by'];
    494         }
    495         if ( ! empty( $request_data['sort'] ) ) {
    496             $args['order'] = $request_data['sort'];
    497         }
    498         if ( ! empty( $request_data['offset'] ) ) {
    499             $args['offset'] = $request_data['offset'];
    500         }
    501 
    502         //get all restricted pages
    503         $restricted_pages_query = new WP_Query( $args );
    504         $restricted_pages = [];
    505         if ( $restricted_pages_query->have_posts() ) {
    506 
    507             //get list of total access of restricted pages
    508             $page_ids = array_column( $restricted_pages_query->posts, 'ID' );
    509             global $wpdb;
    510             $sql = "SELECT page_id, COUNT(*) as totoal_access FROM ". $wpdb->prefix . TABLE_QUENTN_RESTRICTIONS. " where page_id IN (".implode(",",$page_ids).")  GROUP BY page_id";
    511             $rows = $wpdb->get_results( $sql );
    512             $pages_access_links = array();
    513 
    514             foreach ( $rows as $row ) {
    515                 $pages_access_links[$row->page_id] =  $row->totoal_access;
    516             }
    517 
    518             foreach( $restricted_pages_query->posts as $restricted_page ) {
    519                 $quentn_post_restrict_meta = get_post_meta( $restricted_page->ID, '_quentn_post_restrict_meta', true );
    520                 $restricted_pages[] = array(
    521                     "page_id"    => $restricted_page->ID,
    522                     "page_title" => $restricted_page->post_title,
    523                     "page_public_url" => get_page_link( $restricted_page->ID ),
    524                     "restriction_type" =>  ! empty( $quentn_post_restrict_meta['countdown'] ) ? 'countdown' : 'access',
    525                     "access_links" => ( isset( $pages_access_links[$restricted_page->ID] ) ) ? $pages_access_links[$restricted_page->ID] : 0 ,
    526                 );
    527             }
    528         }
    529 
    530         $response = [
    531             'success' => true,
    532             'total' => count( $restricted_pages ),
    533             'limit' => $limit,
    534             'offset' =>  ! empty( $request_data['offset'] ) ? $request_data['offset'] : 0,
    535             'order_by' => ! empty( $request_data['order_by'] ) ? $request_data['order_by'] : 'date',
    536             'sort' => ! empty( $request_data['sort'] ) ? $request_data['sort'] : 'DESC',
    537             'data' => $restricted_pages,
    538         ];
    539         return rest_ensure_response( $response );
     509        global $wpdb;
     510
     511        $request_body = json_decode( $request->get_body(), true );
     512        $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
     513        $request_data = $request_body_data['data'];
     514
     515        // Sanitize and validate parameters
     516        $allowed_orderby = [ 'title', 'date', 'modified', 'menu_order' ];
     517        $allowed_sort    = [ 'ASC', 'DESC' ];
     518
     519        $limit     = isset( $request_data['limit'] ) ? absint( $request_data['limit'] ) : 50;
     520        $offset    = isset( $request_data['offset'] ) ? absint( $request_data['offset'] ) : 0;
     521        $order_by = ( isset( $request_data['order_by'] ) && in_array( $request_data['order_by'], $allowed_orderby ) )  ? $request_data['order_by']  : 'date';
     522        $sort = ( isset( $request_data['sort'] ) && in_array( strtoupper( $request_data['sort'] ), $allowed_sort ) )  ? strtoupper( $request_data['sort'] ) : 'DESC';
     523
     524
     525        $args = [
     526            'post_type'      => 'page',
     527            'meta_key'       => '_quentn_post_restrict_meta',
     528            'orderby'        => $order_by,
     529            'order'          => $sort,
     530            'offset'         => $offset,
     531            'posts_per_page' => $limit,
     532        ];
     533
     534        // Query restricted pages
     535        $restricted_pages_query = new WP_Query( $args );
     536        $restricted_pages = [];
     537
     538        if ( $restricted_pages_query->have_posts() ) {
     539            $page_ids = array_column( $restricted_pages_query->posts, 'ID' );
     540
     541            $pages_access_links = [];
     542
     543            if ( ! empty( $page_ids ) ) {
     544                $placeholders = implode( ',', array_fill( 0, count( $page_ids ), '%d' ) );
     545                $sql = $wpdb->prepare(
     546                    "SELECT page_id, COUNT(*) as totoal_access
     547                 FROM {$wpdb->prefix}" . TABLE_QUENTN_RESTRICTIONS . "
     548                 WHERE page_id IN ($placeholders)
     549                 GROUP BY page_id",
     550                    $page_ids
     551                );
     552                $rows = $wpdb->get_results( $sql );
     553
     554                foreach ( $rows as $row ) {
     555                    $pages_access_links[ $row->page_id ] = $row->totoal_access;
     556                }
     557            }
     558
     559            foreach ( $restricted_pages_query->posts as $restricted_page ) {
     560                $quentn_post_restrict_meta = get_post_meta( $restricted_page->ID, '_quentn_post_restrict_meta', true );
     561
     562                $restricted_pages[] = [
     563                    'page_id'          => $restricted_page->ID,
     564                    'page_title'       => $restricted_page->post_title,
     565                    'page_public_url'  => get_page_link( $restricted_page->ID ),
     566                    'restriction_type' => ! empty( $quentn_post_restrict_meta['countdown'] ) ? 'countdown' : 'access',
     567                    'access_links'     => $pages_access_links[ $restricted_page->ID ] ?? 0,
     568                ];
     569            }
     570        }
     571        $response = [
     572            'success'   => true,
     573            'total'     => count( $restricted_pages ),
     574            'limit'     => $limit,
     575            'offset'    => $offset,
     576            'order_by'  => $order_by,
     577            'sort'      => $sort,
     578            'data'      => $restricted_pages,
     579        ];
     580
     581        return rest_ensure_response( $response );
    540582    }
    541583
     
    550592        $wp_roles = new WP_Roles();
    551593        $all_roles = $wp_roles->get_names();
    552         //todo remove json_encode function
     594        //todo remove json_encode function
    553595        return rest_ensure_response( json_encode( $all_roles ) );
    554596    }
     
    587629                    update_user_meta( $user_id, $meta_key, $meta_value );
    588630                }
    589                 do_action( 'quentn_user_updated', $qn_userdata['user_email'], $user_id );
     631                do_action( 'quentn_user_updated', $qn_userdata['user_email'], $user_id );
    590632            } else {
    591633                //no default role set
     
    596638                if ( ! is_wp_error( $user_id ) ) {
    597639                    update_user_meta( $user_id, 'quentn_last_login', 0 );
    598                     do_action( 'quentn_user_created', $qn_userdata['user_email'], $user_id );
     640                    do_action( 'quentn_user_created', $qn_userdata['user_email'], $user_id );
    599641                }
    600642            }
     
    611653                $new_roles = $request_data['data']['roles']['add_roles'];
    612654                foreach ( $new_roles as $new_role ) {
    613                     $new_user->add_role( trim( $new_role ) );
    614                     do_action( 'quentn_user_role_added', $new_user->user_email, $user_id, trim( $new_role ) );
     655                    $role_to_add  = trim( $new_role );
     656
     657                    // Skip if the role is 'administrator'
     658                    if ( strtolower($role_to_add) === 'administrator' ) {
     659                        continue;
     660                    }
     661
     662                    $new_user->add_role( $role_to_add );
     663                    do_action( 'quentn_user_role_added', $new_user->user_email, $user_id, trim( $new_role ) );
    615664                }
    616665            }
     
    621670                foreach ( $remove_roles as $remove_role ) {
    622671                    $new_user->remove_role( trim( $remove_role ) );
    623                     do_action( 'quentn_user_role_removed', $new_user->user_email, $user_id, trim( $remove_role ) );
     672                    do_action( 'quentn_user_role_removed', $new_user->user_email, $user_id, trim( $remove_role ) );
    624673                }
    625674            }
     
    656705    }
    657706
    658     /**
    659      * Get list of logs
    660      *
    661      * @since  1.2.8
    662      * @access public
    663      * @param WP_REST_Request $request The current request object.
    664      * @return WP_Error|WP_REST_Response
    665      */
    666     public function quentn_get_logs( $request ) {
    667         $request_body = json_decode( $request->get_body(), true );
    668         $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
    669         $request_data = $request_body_data['data'];
    670         $conditions = [];
    671         $response = [];
    672 
    673         global $wpdb;
    674         $sql = "SELECT * FROM " . $wpdb->prefix . TABLE_QUENTN_LOG ;
    675 
    676         if ( ! empty( $request_data['events'] ) ) {
    677             $conditions[] = "event IN (" . implode(',', array_map('intval', $request_data['events'] ) ) . ")";
    678         }
    679 
    680         if ( ! empty( $request_data['emails'] ) ) {
    681             $escaped_emails = array_map( function( $val ) use ( $wpdb ) {
    682                 return $wpdb->prepare('%s', $val);
    683             }, $request_data['emails'] );
    684             $conditions[] = "email IN (" . implode(',', $escaped_emails) . ")";
    685         }
    686 
    687         if ( ! empty( $request_data['pages'] ) ) {
    688             $conditions[] = "page_id IN (" . implode(',', array_map('intval', $request_data['pages'] ) ) . ")";
    689         }
    690 
    691         if ( ! empty( $request_data['from'] ) ) {
    692             $conditions[] = "created_at >= ". intval( $request_data['from'] );
    693         }
    694 
    695         if ( ! empty( $request_data['to'] ) ) {
    696             $conditions[] = "created_at <= ". intval( $request_data['to'] );
    697         }
    698 
    699         if ( ! empty( $conditions ) ) {
    700             $sql .= " WHERE " . implode( " AND ", $conditions );
    701         }
    702 
    703         //order by
    704         $order_by = ! empty( $request_data['order_by'] ) ? $request_data['order_by'] : 'created_at';
    705         $sort_by = ! empty( $request_data['sort'] ) ? $request_data['sort'] : 'desc';
    706         $sql .= " order by ". $order_by. " ". $sort_by;
    707 
    708         //limit
    709         $limit = ! empty( $request_data['limit'] ) ? intval( $request_data['limit'] ) : 50;
    710         $offset = ! empty( $request_data['offset'] ) ? intval( $request_data['offset'] ) : 0;
    711         $sql .= " limit ". $offset . ", " . $limit;
    712 
    713         $results = $wpdb->get_results( $sql, 'ARRAY_A' );
    714         if ( $wpdb->last_error ) {
    715             return new WP_Error( 'log_call_failed', $wpdb->last_error );
    716         }
    717 
    718         //prepare response data key
    719         $logs = [];
    720         foreach ( $results as $log ) {
    721             if ( ! empty( $log['page_id'] ) ) {
    722                 $log['page_title'] = get_the_title( $log['page_id'] );
    723                 $log['page_public_url'] = get_page_link( $log['page_id'] );
    724             } else {
    725                 $log['page_title'] = '';
    726                 $log['page_public_url'] = '';
    727             }
    728 
    729             $logs[] = $log;
    730         }
    731 
    732         include_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/config.php';
    733         if ( ! empty( $request_data['events'] ) ) {
    734             $requested_events = [];
    735             foreach ( $request_data['events'] as $event ) {
    736                 $requested_events[$event] = $events[ $event ];
    737             }
    738         }
    739         $response = [
    740             'success' => true,
    741             'total' => count( $results ),
    742             'limit' => $limit,
    743             'offset' => $offset,
    744             'order_by' => $order_by,
    745             'sort' => $sort_by,
    746             'events' => $events,
    747             'requested_events' => $requested_events,
    748             'data' => $logs,
    749         ];
    750 
    751         return rest_ensure_response( $response );
    752     }
    753     /**
    754      * Get list of get page access
    755      *
    756      * @since  1.2.8
    757      * @access public
    758      * param WP_REST_Request $request The current request object.
    759      * @return WP_Error|WP_REST_Response
    760      */
    761     public function quentn_get_page_access( $request ) {
    762         $request_body = json_decode( $request->get_body(), true );
    763         $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
    764         $request_data = $request_body_data['data'];
    765 
    766         $page_id = $request->get_param('pid');
    767 
    768         global $wpdb;
    769         $sql = "SELECT email, email_hash, created_at FROM " . $wpdb->prefix . TABLE_QUENTN_RESTRICTIONS. " where page_id='". $page_id . "'";
    770 
    771         //order by
    772         $order_by = ! empty( $request_data['order_by'] ) ? $request_data['order_by'] : 'email';
    773         $sort_by = ! empty( $request_data['sort'] ) ? $request_data['sort'] : 'desc';
    774         $sql .= " order by ". $order_by. " ". $sort_by;
    775 
    776         //limit
    777         $limit = ! empty( $request_data['limit'] ) ? intval( $request_data['limit'] ) : 50;
    778         $offset = ! empty( $request_data['offset'] ) ? intval( $request_data['offset'] ) : 0;
    779         $sql .= " limit ". $offset . ", " . $limit;
    780 
    781         $results = $wpdb->get_results( $sql, 'ARRAY_A' );
    782         if ( $wpdb->last_error ) {
    783             return new WP_Error( 'page_access_call_failed', $wpdb->last_error );
    784         }
    785 
    786         //prepare response data
    787         $page_accesses = [];
    788         $separator = ( parse_url( get_page_link( $page_id ), PHP_URL_QUERY ) ) ? '&' : '?';
    789         foreach ( $results as $page_access ) {
    790             $page_access['access_link'] = get_page_link( $page_id ) . $separator.'qntn_wp=' . $page_access['email_hash'];
    791             unset( $page_access['email_hash'] ); //email not included in response
    792             $page_accesses[] = $page_access;
    793         }
    794 
    795         $response = [
    796             'success' => true,
    797             'page_id' => $page_id,
    798             'page_title' => get_the_title( $page_id ),
    799             'page_public_url' => get_page_link( $page_id ),
    800             'total' => count( $page_accesses ),
    801             'limit' => $limit,
    802             'offset' => $offset,
    803             'order_by' => $order_by,
    804             'sort' => $sort_by,
    805             'data' => $page_accesses,
    806         ];
    807 
    808         return rest_ensure_response( $response );
    809     }
    810 
    811     /**
    812      * Get list of get page restriction settings
    813      *
    814      * @since  1.2.8
    815      * @access public
    816      * @param WP_REST_Request $request The current request object.
    817      * @return WP_Error|WP_REST_Response
    818      */
    819     public function quentn_get_page_restriction_settings( $request ) {
    820 
    821         $page_id = $request->get_param('pid');
    822 
    823         $restricted_data = get_post_meta( $page_id, '_quentn_post_restrict_meta', true );
    824 
    825         $response[] = true;
    826         $response = [
    827             'success' => true,
    828             'page_id' => $page_id,
    829             'page_title' => get_the_title( $page_id ),
    830             'page_public_url' => get_page_link( $page_id ),
    831         ];
    832         $response['restriction_enabled'] = boolval( $restricted_data['status'] );
    833         if ( ! empty( $restricted_data ) ) {
    834             $response['restriction_type'] = ! empty( $restricted_data['countdown'] ) ? 'countdown' : 'access';
    835             $response['countdown_type'] = $restricted_data['countdown_type'];
    836             $response['countdown_absolute_date'] = $restricted_data['absolute_date'];
    837             $response['countdown_relative_settings'] = [
    838                 'hours' => $restricted_data['hours'],
    839                 'minutes' => $restricted_data['minutes'],
    840                 'seconds' => $restricted_data['seconds'],
    841             ];
    842             $response['countdown_relative_start_type'] = $restricted_data['access_mode'] == 'permission_granted_mode' ? 'permission_granted' : 'first_visit';
    843             $response['display_countdown'] = $restricted_data['display_countdown_default_status'];
    844             $response['countdown_top_page'] = $restricted_data['quentn_countdown_stick_on_top'];
    845             $response['redirection_type'] = $restricted_data['redirection_type'] == 'restricted_message' ? 'message' : 'url';
    846             $response['redirection_url'] = $restricted_data['redirect_url'];
    847             $response['redirection_message'] = $restricted_data['error_message'];
    848         }
    849 
    850         return rest_ensure_response( $response );
    851     }
     707    /**
     708     * Get list of logs
     709     *
     710     * @since  1.2.8
     711     * @access public
     712     * @param WP_REST_Request $request The current request object.
     713     * @return WP_Error|WP_REST_Response
     714     */
     715    public function quentn_get_logs( $request ) {
     716        global $wpdb;
     717
     718        $request_body = json_decode( $request->get_body(), true );
     719        $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
     720
     721        if ( ! isset( $request_body_data['data'] ) || ! is_array( $request_body_data['data'] ) ) {
     722            return new WP_Error( 'invalid_data', __( 'Malformed data structure.', 'quentn-wp' ), [ 'status' => 400 ] );
     723        }
     724        $request_data = $request_body_data['data'];
     725
     726        $conditions = [];
     727        $values = [];
     728
     729        $sql = "SELECT * FROM {$wpdb->prefix}" . TABLE_QUENTN_LOG;
     730
     731        // Safe filtering
     732        if ( ! empty( $request_data['events'] ) ) {
     733            $event_ids = array_map( 'intval', $request_data['events'] );
     734            $placeholders = implode( ',', array_fill( 0, count( $event_ids ), '%d' ) );
     735            $conditions[] = "event IN ($placeholders)";
     736            $values = array_merge( $values, $event_ids );
     737        }
     738
     739        if ( ! empty( $request_data['emails'] ) ) {
     740            $email_placeholders = implode( ',', array_fill( 0, count( $request_data['emails'] ), '%s' ) );
     741            $conditions[] = "email IN ($email_placeholders)";
     742            $values = array_merge( $values, $request_data['emails'] );
     743        }
     744
     745        if ( ! empty( $request_data['pages'] ) ) {
     746            $page_ids = array_map( 'intval', $request_data['pages'] );
     747            $placeholders = implode( ',', array_fill( 0, count( $page_ids ), '%d' ) );
     748            $conditions[] = "page_id IN ($placeholders)";
     749            $values = array_merge( $values, $page_ids );
     750        }
     751
     752        if ( ! empty( $request_data['from'] ) ) {
     753            $conditions[] = "created_at >= %d";
     754            $values[] = intval( $request_data['from'] );
     755        }
     756
     757        if ( ! empty( $request_data['to'] ) ) {
     758            $conditions[] = "created_at <= %d";
     759            $values[] = intval( $request_data['to'] );
     760        }
     761
     762        if ( ! empty( $conditions ) ) {
     763            $sql .= " WHERE " . implode( " AND ", $conditions );
     764        }
     765
     766        // Validate and whitelist sort fields
     767        $allowed_order_by = [ 'created_at', 'event', 'email', 'page_id' ];
     768        $allowed_sort = [ 'ASC', 'DESC' ];
     769
     770        $order_by = ( isset( $request_data['order_by'] ) && in_array( $request_data['order_by'], $allowed_order_by ) ) ? $request_data['order_by'] : 'created_at';
     771        $sort_by = ( isset( $request_data['sort'] ) && in_array( strtoupper( $request_data['sort'] ), $allowed_sort ) ) ? strtoupper( $request_data['sort'] ) : 'DESC';
     772
     773        $sql .= " ORDER BY $order_by $sort_by";
     774
     775        // Limit + offset
     776        $limit = ! empty( $request_data['limit'] ) ? absint( $request_data['limit'] ) : 50;
     777        $offset = ! empty( $request_data['offset'] ) ? absint( $request_data['offset'] ) : 0;
     778
     779        $sql .= " LIMIT %d OFFSET %d";
     780        $values[] = $limit;
     781        $values[] = $offset;
     782
     783        // Prepare and execute query
     784        $prepared_sql = $wpdb->prepare( $sql, $values );
     785        $results = $wpdb->get_results( $prepared_sql, 'ARRAY_A' );
     786
     787        if ( $wpdb->last_error ) {
     788            return new WP_Error( 'log_call_failed', $wpdb->last_error );
     789        }
     790
     791        // Process results
     792        $logs = [];
     793        foreach ( $results as $log ) {
     794            if ( ! empty( $log['page_id'] ) ) {
     795                $log['page_title'] = get_the_title( $log['page_id'] );
     796                $log['page_public_url'] = get_page_link( $log['page_id'] );
     797            } else {
     798                $log['page_title'] = '';
     799                $log['page_public_url'] = '';
     800            }
     801
     802            $logs[] = $log;
     803        }
     804
     805        // Load event labels
     806        include_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/config.php';
     807
     808        $requested_events = [];
     809        if ( ! empty( $request_data['events'] ) ) {
     810            foreach ( $request_data['events'] as $event ) {
     811                if ( isset( $events[ $event ] ) ) {
     812                    $requested_events[ $event ] = $events[ $event ];
     813                }
     814            }
     815        }
     816
     817        $response = [
     818            'success'          => true,
     819            'total'            => count( $results ),
     820            'limit'            => $limit,
     821            'offset'           => $offset,
     822            'order_by'         => $order_by,
     823            'sort'             => $sort_by,
     824            'events'           => $events,
     825            'requested_events' => $requested_events,
     826            'data'             => $logs,
     827        ];
     828
     829        return rest_ensure_response( $response );
     830    }
     831
     832    /**
     833     * Get list of get page access
     834     *
     835     * @since  1.2.8
     836     * @access public
     837     * param WP_REST_Request $request The current request object.
     838     * @return WP_Error|WP_REST_Response
     839     */
     840    public function quentn_get_page_access( $request ) {
     841        $request_body = json_decode( $request->get_body(), true );
     842        $request_body_data = json_decode( base64_decode( $request_body['data'] ), true );
     843        $request_data = $request_body_data['data'];
     844        $page_id = $request->get_param('pid');
     845
     846        global $wpdb;
     847        $table_name = $wpdb->prefix . TABLE_QUENTN_RESTRICTIONS;
     848
     849        // Validation against allowed values
     850        $allowed_order_by = ['email', 'email_hash', 'created_at'];
     851        $order_by = in_array($request_data['order_by'], $allowed_order_by) ? $request_data['order_by'] : 'created_at';
     852
     853        $allowed_sorts = ['asc', 'desc'];
     854        $sort_by = in_array(strtolower($request_data['sort']), $allowed_sorts) ? $request_data['sort'] : 'desc';
     855
     856        $limit = isset($request_data['limit']) ? intval($request_data['limit']) : 50;
     857        $offset = isset($request_data['offset']) ? intval($request_data['offset']) : 0;
     858
     859        $sql = "SELECT email, email_hash, created_at FROM $table_name WHERE page_id = %d ORDER BY $order_by $sort_by LIMIT %d, %d";
     860
     861        $results = $wpdb->get_results($wpdb->prepare($sql, $page_id, $offset, $limit), ARRAY_A);
     862        if ($wpdb->last_error) {
     863            return new WP_Error('page_access_call_failed', $wpdb->last_error);
     864        }
     865
     866
     867        //prepare response data
     868        $page_accesses = [];
     869        $separator = ( parse_url( get_page_link( $page_id ), PHP_URL_QUERY ) ) ? '&' : '?';
     870        foreach ( $results as $page_access ) {
     871            $page_access['access_link'] = get_page_link( $page_id ) . $separator.'qntn_wp=' . $page_access['email_hash'];
     872            unset( $page_access['email_hash'] ); //email not included in response
     873            $page_accesses[] = $page_access;
     874        }
     875
     876        $response = [
     877            'success' => true,
     878            'page_id' => $page_id,
     879            'page_title' => get_the_title( $page_id ),
     880            'page_public_url' => get_page_link( $page_id ),
     881            'total' => count( $page_accesses ),
     882            'limit' => $limit,
     883            'offset' => $offset,
     884            'order_by' => $order_by,
     885            'sort' => $sort_by,
     886            'data' => $page_accesses,
     887        ];
     888
     889        return rest_ensure_response( $response );
     890    }
     891
     892    /**
     893     * Get list of get page restriction settings
     894     *
     895     * @since  1.2.8
     896     * @access public
     897     * @param WP_REST_Request $request The current request object.
     898     * @return WP_Error|WP_REST_Response
     899     */
     900    public function quentn_get_page_restriction_settings( $request ) {
     901
     902        $page_id = absint( $request->get_param('pid') );
     903
     904        if ( ! $page_id ) {
     905            return new WP_Error( 'invalid_page_id', __( 'Page ID is required.', 'quentn-wp' ), [ 'status' => 400 ] );
     906        }
     907
     908        $restricted_data = get_post_meta( $page_id, '_quentn_post_restrict_meta', true );
     909
     910        $response[] = true;
     911        $response = [
     912            'success' => true,
     913            'page_id' => $page_id,
     914            'page_title' => get_the_title( $page_id ),
     915            'page_public_url' => get_page_link( $page_id ),
     916        ];
     917        $response['restriction_enabled'] = boolval( $restricted_data['status'] );
     918        if ( ! empty( $restricted_data ) ) {
     919            $response['restriction_type'] = ! empty( $restricted_data['countdown'] ) ? 'countdown' : 'access';
     920            $response['countdown_type'] = $restricted_data['countdown_type'];
     921            $response['countdown_absolute_date'] = $restricted_data['absolute_date'];
     922            $response['countdown_relative_settings'] = [
     923                'hours' => $restricted_data['hours'],
     924                'minutes' => $restricted_data['minutes'],
     925                'seconds' => $restricted_data['seconds'],
     926            ];
     927            $response['countdown_relative_start_type'] = $restricted_data['access_mode'] == 'permission_granted_mode' ? 'permission_granted' : 'first_visit';
     928            $response['display_countdown'] = $restricted_data['display_countdown_default_status'];
     929            $response['countdown_top_page'] = $restricted_data['quentn_countdown_stick_on_top'];
     930            $response['redirection_type'] = $restricted_data['redirection_type'] == 'restricted_message' ? 'message' : 'url';
     931            $response['redirection_url'] = $restricted_data['redirect_url'];
     932            $response['redirection_message'] = $restricted_data['error_message'];
     933        }
     934
     935        return rest_ensure_response( $response );
     936    }
    852937
    853938    /**
     
    896981    }
    897982
    898 
    899983    /**
    900984     * Varify quentn request
     
    906990     */
    907991    public function quentn_check_credentials( $request ) {
    908 
    909992        $request_body = json_decode( $request->get_body(), true );
    910993
    911         $api_key = ( get_option('quentn_app_key') ) ? get_option('quentn_app_key') : '';
    912 
    913         //check time validation for request
     994        // Basic input validation
     995        if ( !isset( $request_body['vu'] ) || !isset( $request_body['hash'] ) ) {
     996            return new WP_Error( 'missing_params', esc_html__( 'Required parameters missing', 'quentn-wp' ), array( 'status' => 400 ) );
     997        }
     998
     999        $api_key = get_option( 'quentn_app_key', '' );
     1000
     1001        // Return error if API key is empty (critical security check)
     1002        if ( empty( $api_key ) ) {
     1003            return new WP_Error( 'api_key_not_set', esc_html__( 'API key is not configured', 'quentn-wp' ), array( 'status' => 401 ) );
     1004        }
     1005
     1006        // validate time
    9141007        if( $request_body['vu'] <= time() ) {
    9151008            return new WP_Error( 'time_expired', esc_html__( 'Time has expired', 'quentn-wp' ), array( 'status' => 401 ) );
    9161009        }
    9171010
    918 
    919         if( isset( $request_body['data'] ) ) {
    920             $hash =  hash( 'sha256', $request_body['data'].$request_body['vu'].$api_key );
    921         } else {
    922             $hash =  hash( 'sha256', $request_body['vu'].$api_key );
    923         }
    924 
    925         if ( $hash != $request_body['hash'] ) {
    926             return new WP_Error( 'invalid_key', esc_html__( 'Incorrect Api Key', 'quentn-wp' ), array( 'status' => 401 ) );
     1011        // Calculate expected hash
     1012        $components = [ $request_body['vu'], $api_key ];
     1013        if ( isset( $request_body['data'] ) ) {
     1014            array_unshift( $components, $request_body['data'] );
     1015        }
     1016
     1017        // Use hash_equals() for timing-safe comparison
     1018        $expected_hash = hash( 'sha256', implode( '', $components ) );
     1019        if ( !hash_equals( $expected_hash, $request_body['hash'] ) ) {
     1020            return new WP_Error( 'invalid_key', esc_html__( 'Authentication failed', 'quentn-wp' ), array( 'status' => 401 ) );
    9271021        }
    9281022
  • quentn-wp/trunk/includes/class-quentn-wp.php

    r3073163 r3266307  
    7575            $this->version = QUENTN_WP_VERSION;
    7676        } else {
    77             $this->version = '1.2.8';
     77            $this->version = '1.2.9';
    7878        }
    7979        $this->plugin_name = 'quentn-wp';
  • quentn-wp/trunk/quentn-wp.php

    r3073163 r3266307  
    3535define( "QUENTN_WP_PLUGIN_DIR", plugin_dir_path( __FILE__ ) );
    3636define( "QUENTN_WP_PLUGIN_URL", plugin_dir_url(  __FILE__ ) );
    37 define( 'QUENTN_WP_VERSION', '1.2.8' );
     37define( 'QUENTN_WP_VERSION', '1.2.9' );
    3838define( 'QUENTN_WP_DB_VERSION', '1.1' );
    3939
  • quentn-wp/trunk/readme.txt

    r3073185 r3266307  
    33Tags: Quentn, countdown, page restriction, email, marketing automation
    44Requires at least: 4.6.0
    5 Tested up to: 6.4
    6 Stable tag: 1.2.8
     5Tested up to: 6.7.2
     6Stable tag: 1.2.9
    77Requires PHP: 5.6.0
    88License: GPLv2 or later
     
    6767== Changelog ==
    6868
     69= 1.2.9 =
     70* Security Fix: Fixed SQL injection vulnerabilities
     71* Security Fix: Hardened input validation for all admin operations
     72* Security Fix: Improved data sanitization and escaping
     73
    6974= 1.2.8 =
    7075* Add: Log option to trace different user quentn related activities.
     
    164169== Upgrade Notice ==
    165170
     171= 1.2.9 - Security Update =
     172This is a critical security update. It addresses potential SQL injection vulnerabilities found in previous versions. Please update immediately to ensure your site remains secure.
     173
    166174= 1.2.8 =
    167175Thanks for using Quentn Plugin! Please update the plugin to add log quentn activities. It will also fix any namespace conflict with other plugins.
Note: See TracChangeset for help on using the changeset viewer.