Plugin Directory

Changeset 3484635


Ignore:
Timestamp:
03/17/2026 10:04:29 AM (11 days ago)
Author:
2winfactor
Message:

Update to version 4.1.1 from GitHub

Location:
presto-player
Files:
20 edited
1 copied

Legend:

Unmodified
Added
Removed
  • presto-player/tags/4.1.1/dist/components/stats.json

    r3468484 r3484635  
    11{
    2   "timestamp": "2026-02-24T09:46:04",
     2  "timestamp": "2026-03-17T10:02:49",
    33  "compiler": {
    44    "name": "node",
    5     "version": "20.20.0"
     5    "version": "20.20.1"
    66  },
    77  "app": {
  • presto-player/tags/4.1.1/inc/Factory.php

    r3468484 r3484635  
    88use PrestoPlayer\Support\Block;
    99use PrestoPlayer\Services\Scripts;
    10 use PrestoPlayer\Services\BunnyCDN;
     10// Disabled: PrestoPlayer\Services\BunnyCDN class does not exist. Kept for reference.
     11// use PrestoPlayer\Services\BunnyCDN;
    1112use PrestoPlayer\Services\Settings;
    1213use PrestoPlayer\Services\AdminNotices;
     
    3637    public function getRules() {
    3738        return array(
    38             BunnyCDN::class       => self::SHARED,
     39            // Disabled: BunnyCDN service class does not exist; this was a stale reference.
     40            // BunnyCDN::class       => self::SHARED,
    3941            Visits::class         => self::SHARED,
    4042            ReusableVideos::class => self::SHARED,
  • presto-player/tags/4.1.1/inc/Integrations/LearnDash/LearnDash.php

    r3317175 r3484635  
    4141        ob_start();
    4242        ?>
    43         data-video-cookie-key="<?php echo $data['cookieKey']; ?>"
    44         data-video-progression="<?php echo $data['videoProgress']; ?>"
     43        data-video-cookie-key="<?php echo esc_attr( $data['cookieKey'] ); ?>"
     44        data-video-progression="<?php echo esc_attr( $data['videoProgress'] ); ?>"
    4545        data-video-provider="presto"
    4646        <?php
  • presto-player/tags/4.1.1/inc/Libraries/BunnyCDN.php

    r3122484 r3484635  
    1 <?php
     1<?php // phpcs:ignoreFile -- Third-party BunnyCDN SDK library; WPCS enforcement would require full rewrite.
    22
    33namespace PrestoPlayer\Libraries;
    44
     5/**
     6 * Third-party BunnyCDN SDK library.
     7 *
     8 * NOTE: This file is NOT actively used — no code in either the free (Presto Player)
     9 * or pro (Presto Player Pro) plugin instantiates or calls this class.
     10 * It is kept here for future reference only.
     11 *
     12 * @see https://github.com/prestomade/presto-player/issues/898
     13 * @see https://github.com/prestomade/presto-player/issues/899
     14 */
    515class BunnyCDN {
    616
     
    12151225        $file = $api_call['data'];
    12161226
    1217         header( 'Content-type: application/octet-stream' );
    1218         header( "Content-Disposition: attachment; filename=$file_name" );
    1219         // will force to download...
     1227        $safe_name = str_replace( '"', '', sanitize_file_name( $file_name ) );
     1228        header( 'Content-Type: application/octet-stream' );
     1229        header( 'Content-Disposition: attachment; filename="' . $safe_name . '"' );
    12201230        echo $file;
    12211231    }
     
    12981308
    12991309    public function DownloadFile( $file_url = '', $oupt_file_name = '' ) {
    1300         // this is a fast way to download a file
    1301         // remove any query string data
    1302         if ( isset( $oupt_file_name ) ) {
     1310        $parsed = wp_parse_url( $file_url );
     1311        if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], array( 'http', 'https' ), true ) ) {
     1312            wp_die( 'Invalid file URL.', 'Error', array( 'response' => 400 ) );
     1313        }
     1314
     1315        if ( ! empty( $oupt_file_name ) ) {
    13031316            $file_name = $oupt_file_name;
    1304         }
    1305         if ( empty( $oupt_file_name ) ) {
     1317        } else {
    13061318            $file_name = preg_replace( '/\?.*/', '', basename( $file_url ) );
     1319        }
     1320
     1321        $safe_name = str_replace( '"', '', sanitize_file_name( $file_name ) );
     1322
     1323        $response = wp_safe_remote_get( $file_url, array( 'timeout' => 60 ) );
     1324        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
     1325            wp_die( 'Download failed.', 'Error', array( 'response' => 500 ) );
    13071326        }
    13081327
    13091328        header( 'Content-Type: application/octet-stream' );
    13101329        header( 'Content-Transfer-Encoding: Binary' );
    1311         header( "Content-disposition: attachment; filename=$file_name" );
    1312         readfile( $file_url );
     1330        header( 'Content-Disposition: attachment; filename="' . $safe_name . '"' );
     1331        echo wp_remote_retrieve_body( $response );
     1332        exit;
    13131333    }
    13141334
    13151335
    13161336    public function DownloadFile1( $file_url ) {
    1317         /*
    1318             this is a slow way to download a file
    1319             will allow you to download a remote file from any server that is accessible
    1320         */
    1321 
    1322         $filename = $file_url;
    1323         $filedata = @file_get_contents( $filename );
    1324 
    1325         // SUCCESS
    1326         if ( $filedata ) {
    1327             // GET A NAME FOR THE FILE
    1328             // remove any query string data
    1329             $basename = preg_replace( '/\?.*/', '', basename( $file_url ) );
    1330             // $basename = basename($filename);
    1331 
    1332             // THESE HEADERS ARE USED ON ALL BROWSERS
    1333             header( 'Content-Type: application-x/force-download' );
    1334             header( "Content-Disposition: attachment; filename=$basename" );
    1335             header( 'Content-length: ' . (string) ( strlen( $filedata ) ) );
    1336             header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', mktime( date( 'H' ) + 2, date( 'i' ), date( 's' ), date( 'm' ), date( 'd' ), date( 'Y' ) ) ) . ' GMT' );
    1337             header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
    1338 
    1339             // THIS HEADER MUST BE OMITTED FOR IE 6+
    1340             if ( false === strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE ' ) ) {
    1341                 header( 'Cache-Control: no-cache, must-revalidate' );
    1342             }
    1343 
    1344             // THIS IS THE LAST HEADER
    1345             header( 'Pragma: no-cache' );
    1346 
    1347             // FLUSH THE HEADERS TO THE BROWSER
    1348             flush();
    1349 
    1350             // CAPTURE THE FILE IN THE OUTPUT BUFFERS - WILL BE FLUSHED AT SCRIPT END
    1351             ob_start();
    1352             echo $filedata;
    1353         }
    1354 
    1355         // FAILURE
    1356         else {
    1357             die( "ERROR: UNABLE TO OPEN $filename" );
    1358         }
     1337        $parsed = wp_parse_url( $file_url );
     1338        if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], array( 'http', 'https' ), true ) ) {
     1339            wp_die( 'Invalid file URL.', 'Error', array( 'response' => 400 ) );
     1340        }
     1341
     1342        $response = wp_safe_remote_get( $file_url, array( 'timeout' => 60 ) );
     1343        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
     1344            wp_die( 'Download failed.', 'Error', array( 'response' => 500 ) );
     1345        }
     1346
     1347        $filedata = wp_remote_retrieve_body( $response );
     1348        $basename = str_replace( '"', '', sanitize_file_name( preg_replace( '/\?.*/', '', basename( $file_url ) ) ) );
     1349
     1350        header( 'Content-Type: application/octet-stream' );
     1351        header( 'Content-Disposition: attachment; filename="' . $basename . '"' );
     1352        header( 'Content-Length: ' . strlen( $filedata ) );
     1353        header( 'Cache-Control: no-cache, must-revalidate' );
     1354        header( 'Pragma: no-cache' );
     1355
     1356        echo $filedata;
     1357        exit;
    13591358    }
    13601359
  • presto-player/tags/4.1.1/inc/Models/Model.php

    r3122484 r3484635  
    11<?php
     2/**
     3 * Base Model class for interfacing with custom database tables.
     4 *
     5 * @package PrestoPlayer\Models
     6 */
    27
    38namespace PrestoPlayer\Models;
     
    712
    813/**
    9  * Model for interfacing with custom database tables
     14 * Model for interfacing with custom database tables.
    1015 */
    1116abstract class Model implements ModelInterface {
    1217
    1318    /**
    14      * Needs a table name
     19     * Needs a table name.
    1520     *
    1621     * @var string
     
    1924
    2025    /**
    21      * Store model attributes
     26     * Store model attributes.
    2227     *
    2328     * @var object
     
    2631
    2732    /**
    28      * Model schema
     33     * Model schema.
    2934     *
    3035     * @return array
     
    3540
    3641    /**
    37      * Guarded variables
     42     * Guarded variables.
    3843     *
    3944     * @var array
     
    4247
    4348    /**
    44      * Attributes we can query by
     49     * Attributes we can query by.
    4550     *
    4651     * @var array
     
    4954
    5055    /**
    51      * Optionally get something from the db
    52      *
    53      * @param integer $id
     56     * Optionally get something from the db.
     57     *
     58     * @param integer $id Model ID.
    5459     */
    5560    public function __construct( $id = 0 ) {
    5661        $this->attributes = new \stdClass();
    5762        if ( ! empty( $id ) ) {
    58             return $this->set( $this->get( $id )->toObject() );
    59             return $this;
    60         }
    61         return $this;
    62     }
    63 
    64     /**
    65      * Get attributes properties
    66      *
    67      * @param string $property
     63            $this->set( $this->get( $id )->toObject() );
     64        }
     65    }
     66
     67    /**
     68     * Get attributes properties.
     69     *
     70     * @param string $property Property name.
    6871     * @return mixed
    6972     */
     
    7578
    7679    /**
    77      * Get attributes properties
    78      *
    79      * @param string $property
    80      * @return mixed
     80     * Set attributes properties.
     81     *
     82     * @param string $property Property name.
     83     * @param mixed  $value    Property value.
     84     * @return void
    8185     */
    8286    public function __set( $property, $value ) {
     
    8488    }
    8589
     90    /**
     91     * Get the table name.
     92     *
     93     * @return string
     94     */
    8695    public function getTableName() {
    8796        return $this->table;
     
    8998
    9099    /**
    91      * Convert to Object
     100     * Convert to Object.
    92101     *
    93102     * @return object
     
    106115
    107116    /**
    108      * Convert to array
     117     * Convert to array.
    109118     *
    110119     * @return array
     
    115124
    116125    /**
    117      * Formats row data based on schema
    118      *
    119      * @param object $columns
     126     * Formats row data based on schema.
     127     *
     128     * @param object $columns Row columns to format.
    120129     * @return object
    121130     */
     
    136145
    137146    /**
    138      * Fetch all models
    139      *
    140      * @return array Array of preset objects
     147     * Fetch all models.
     148     *
     149     * @return array Array of preset objects.
    141150     */
    142151    public function all() {
    143152        global $wpdb;
    144153
    145         // maybe get only published if we have soft deletes
     154        // Maybe get only published if we have soft deletes.
    146155        $where = ! empty( $this->schema()['deleted_at'] ) ? "WHERE (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00') " : '';
    147156
    148157        $results = $wpdb->get_results(
    149             "SELECT * FROM {$wpdb->prefix}{$this->table} $where"
     158            "SELECT * FROM {$wpdb->prefix}{$this->table} $where" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is a class property. $where is built from safe values.
    150159        );
    151160
     
    154163
    155164    /**
    156      * Fetch models from db
    157      *
    158      * @param array $args
    159      * @return Object Array of models with pagination data
     165     * Fetch models from db.
     166     *
     167     * @param array $args Query arguments.
     168     * @return object|\WP_Error Array of models with pagination data.
    160169     */
    161170    public function fetch( $args = array() ) {
    162171        global $wpdb;
    163172
    164         // remove empties for querying
     173        // Remove empties for querying.
    165174        $args = array_filter(
    166175            wp_parse_args(
     
    175184        );
    176185
    177         // get query args
     186        // Get query args.
    178187        $query = array_filter(
    179188            $args,
    180189            function ( $key ) {
    181                 return in_array( $key, array( 'per_page', 'page', 'status', 'date_query', 'fields', 'order_by' ) );
     190                return in_array( $key, array( 'per_page', 'page', 'status', 'date_query', 'fields', 'order_by' ), true );
    182191            },
    183192            ARRAY_FILTER_USE_KEY
     
    188197
    189198        foreach ( $args as $attribute => $value ) {
    190             // must be queryable and in schema
    191             if ( ! in_array( $attribute, $this->queryable ) || empty( $schema[ $attribute ] ) ) {
     199            // Must be queryable and in schema.
     200            if ( ! in_array( $attribute, $this->queryable, true ) || empty( $schema[ $attribute ] ) ) {
    192201                unset( $args[ $attribute ] );
    193202                continue;
    194203            }
    195204
    196             // attribute schema
     205            // Attribute schema.
    197206            $attr_schema = $schema[ $attribute ];
    198207
    199             // force type
     208            // Force type.
    200209            settype( $value, $attr_schema['type'] );
    201210
    202             // sanitize input
     211            // Sanitize input.
    203212            if ( ! empty( $attr_schema['sanitize_callback'] ) ) {
    204213                $value = $attr_schema['sanitize_callback']( $value );
    205214            }
    206215
    207             // maybe add quotes
    208             if ( in_array( $attr_schema['type'], array( 'integer', 'number', 'boolean' ) ) ) {
    209                 $where .= $wpdb->prepare( 'AND %1s=%2s ', $attribute, $value );
     216            // Column name is already validated against $this->queryable whitelist above.
     217            if ( in_array( $attr_schema['type'], array( 'integer', 'number', 'boolean' ), true ) ) {
     218                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $attribute is validated against queryable whitelist.
     219                $where .= $wpdb->prepare( "AND `{$attribute}` = %d ", $value );
    210220            } else {
    211                 $where .= $wpdb->prepare( "AND %1s='%2s' ", $attribute, $value );
    212             }
    213         }
    214 
    215         // soft deletes
     221                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $attribute is validated against queryable whitelist.
     222                $where .= $wpdb->prepare( "AND `{$attribute}` = %s ", $value );
     223            }
     224        }
     225
     226        // Soft deletes.
    216227        if ( ! empty( $this->schema()['deleted_at'] ) ) {
    217228            $status = ! empty( $args['status'] ) ? $args['status'] : '';
     
    220231                    $where .= "AND (deleted_at IS NOT NULL OR deleted_at != '0000-00-00 00:00:00') ";
    221232                    break;
    222                 default: // default to published
     233                default: // Default to published.
    223234                    $where .= "AND (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00') ";
    224235                    break;
     
    226237        }
    227238
    228         // before and after
     239        // Before and after date queries.
    229240        if ( ! empty( $query['date_query'] ) ) {
    230             // use created at by default
     241            // Use created_at by default.
    231242            $query['date_query'] = wp_parse_args(
    232243                $query['date_query'],
     
    236247            );
    237248
    238             // check for field
    239             $field = ! empty( $this->schema()[ $query['date_query']['field'] ] ) ? sanitize_text_field( $query['date_query']['field'] ) : null;
     249            // Check for valid field.
     250            $field = ! empty( $this->schema()[ $query['date_query']['field'] ] ) ? $query['date_query']['field'] : null;
    240251            if ( ! $field ) {
    241252                return new \WP_Error( 'invalid_field', 'Cannot do a date query by ' . sanitize_text_field( $query['date_query']['field'] ) );
    242253            }
    243254
    244             // if after
     255            // Field name is validated against schema above.
    245256            if ( ! empty( $query['date_query']['after'] ) ) {
    246257                $where .= $wpdb->prepare(
    247                     "AND %1s >= '%2s' ",
    248                     sanitize_text_field( $field ), // i.e. created_at
    249                     date( 'Y-m-d H:i:s', strtotime( $query['date_query']['after'] ) ) // convert to date
     258                    "AND `{$field}` >= %s ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $field is validated against schema whitelist.
     259                    gmdate( 'Y-m-d H:i:s', strtotime( $query['date_query']['after'] ) )
    250260                );
    251261            }
    252             // before
    253262            if ( ! empty( $query['date_query']['before'] ) ) {
    254263                $where .= $wpdb->prepare(
    255                     "AND %1s <= '%2s' ",
    256                     sanitize_text_field( $field ), // i.e. created_at
    257                     date( 'Y-m-d H:i:s', strtotime( $query['date_query']['before'] ) ) // convert to date
     264                    "AND `{$field}` <= %s ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $field is validated against schema whitelist.
     265                    gmdate( 'Y-m-d H:i:s', strtotime( $query['date_query']['before'] ) )
    258266                );
    259267            }
     
    262270        $limit      = (int) $query['per_page'];
    263271        $offset     = (int) ( $query['per_page'] * ( $query['page'] - 1 ) );
    264         $pagination = $wpdb->prepare( 'LIMIT %1s OFFSET %2s ', $limit, $offset );
     272        $pagination = $wpdb->prepare( 'LIMIT %d OFFSET %d ', $limit, $offset );
    265273
    266274        $select = '*';
     
    271279        $order_by = '';
    272280        if ( ! empty( $query['order_by'] ) ) {
    273             $order_by .= 'ORDER BY';
    274             $number    = count( $query['order_by'] );
    275             $i         = 1;
     281            $allowed_dirs  = array( 'ASC', 'DESC' );
     282            $order_clauses = array();
    276283            foreach ( $query['order_by'] as $attribute => $direction ) {
    277                 $order_by .= $wpdb->prepare( ' %1s %2s', $attribute, $direction );
    278                 $order_by .= $i === $number ? '' : ',';
    279                 ++$i;
    280             }
    281             $order_by .= ' ';
    282         }
    283 
    284         $total   = $wpdb->get_var( "SELECT count(id) as count FROM {$wpdb->prefix}{$this->table} $where$order_by" );
     284                if ( ! in_array( $attribute, $this->queryable, true ) && ! array_key_exists( $attribute, $schema ) ) {
     285                    continue;
     286                }
     287                $direction       = in_array( strtoupper( $direction ), $allowed_dirs, true ) ? strtoupper( $direction ) : 'ASC';
     288                $order_clauses[] = "`{$attribute}` {$direction}";
     289            }
     290            if ( ! empty( $order_clauses ) ) {
     291                $order_by = 'ORDER BY ' . implode( ', ', $order_clauses ) . ' ';
     292            }
     293        }
     294
     295        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $where, $order_by, $pagination are built from safe values above.
     296        $total = $wpdb->get_var( "SELECT count(id) as count FROM {$wpdb->prefix}{$this->table} $where" );
     297        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $select, $where, $order_by, $pagination are built from safe values above.
    285298        $results = $wpdb->get_results( "SELECT $select FROM {$wpdb->prefix}{$this->table} $where$order_by$pagination" );
    286299
     
    294307
    295308    /**
    296      * Find a specific model based on query
     309     * Find a specific model based on query.
     310     *
     311     * @param array $args Query arguments.
     312     * @return Model|false
    297313     */
    298314    public function findWhere( $args = array() ) {
     
    303319
    304320    /**
    305      * Turns raw sql query results into models
    306      *
    307      * @param array $results
    308      * @return array Array of Models
     321     * Turns raw sql query results into models.
     322     *
     323     * @param array $results Raw database results.
     324     * @return array Array of Models.
    309325     */
    310326    protected function parseResults( $results ) {
     
    317333
    318334        $output = array();
    319         // return new models for each row
     335        // Return new models for each row.
    320336        foreach ( $results as $result ) {
    321337            $class    = get_class( $this );
     
    326342    }
    327343
     344    /**
     345     * Parse results into an array of IDs.
     346     *
     347     * @param array $results Raw database results.
     348     * @return array Array of integer IDs.
     349     */
    328350    public function parseIds( $results ) {
    329351        if ( is_wp_error( $results ) ) {
     
    342364
    343365    /**
    344      * Gets fresh data from the db
     366     * Gets fresh data from the db.
    345367     *
    346368     * @return Model
     
    354376
    355377    /**
    356      * Get default values set from scheam
     378     * Get default values set from schema.
    357379     *
    358380     * @return array
     
    372394
    373395    /**
    374      * Unset guarded variables
    375      *
    376      * @param array $args
    377      * @return void
     396     * Unset guarded variables.
     397     *
     398     * @param array $args Arguments to filter.
     399     * @return array
    378400     */
    379401    protected function unsetGuarded( $args = array() ) {
    380         // unset guarded
     402        // Unset guarded.
    381403        foreach ( $this->guarded as $arg ) {
    382404            if ( $args[ $arg ] ) {
     
    385407        }
    386408
    387         // we should never set an ID
     409        // We should never set an ID.
    388410        unset( $args['id'] );
    389411
     
    392414
    393415    /**
    394      * Create a preset
    395      *
    396      * @param array $args
     416     * Create a model.
     417     *
     418     * @param array $args Attributes for the new model.
    397419     * @return integer
    398420     */
     
    400422        global $wpdb;
    401423
    402         // unset guarded args
     424        // Unset guarded args.
    403425        $args = $this->unsetGuarded( $args );
    404426
    405         // parse args with default args
     427        // Parse args with default args.
    406428        $args = wp_parse_args( $args, $this->getDefaults() );
    407429
    408         // creation time
     430        // Creation time.
    409431        if ( ! empty( $this->schema()['created_at'] ) ) {
    410432            $args['created_at'] = ! empty( $args['created_at'] ) ? $args['created_at'] : current_time( 'mysql' );
    411433        }
    412434
    413         // maybe serialize args
     435        // Maybe serialize args.
    414436        $args = $this->maybeSerializeArgs( $args );
    415437
    416         // insert
     438        // Insert.
    417439        $wpdb->insert( $wpdb->prefix . $this->table, $args );
    418440
    419         // set ID in attributes
     441        // Set ID in attributes.
    420442        $this->attributes->id = $wpdb->insert_id;
    421443
    422         // created action
     444        // Created action.
    423445        do_action( "{$this->table}_created", $this );
    424446
    425         // return id
     447        // Return id.
    426448        return $this->attributes->id;
    427449    }
    428450
     451    /**
     452     * Maybe serialize array arguments.
     453     *
     454     * @param array $args Arguments to process.
     455     * @return array
     456     */
    429457    protected function maybeSerializeArgs( $args ) {
    430458        foreach ( $args as $key => $arg ) {
     
    438466    }
    439467
     468    /**
     469     * Maybe unserialize array arguments.
     470     *
     471     * @param array $args Arguments to process.
     472     * @return array
     473     */
    440474    protected function maybeUnSerializeArgs( $args ) {
    441475        foreach ( $args as $key => $arg ) {
     
    456490     * argument with the optional second array argument.
    457491     *
    458      * @param array $search Model to search for
    459      * @param array $create Attributes to create
     492     * @param array $search Model to search for.
     493     * @param array $create Attributes to create.
    460494     * @return Model|\WP_Error
    461495     */
     
    470504        }
    471505
    472         // already created
     506        // Already created.
    473507        if ( ! empty( $models->data[0] ) ) {
    474508            $this->set( $models->data[0]->toObject() );
     
    476510        }
    477511
    478         // merge and create
     512        // Merge and create.
    479513        $merged = array_merge( $search, $create );
    480514        $this->create( $merged );
    481515
    482         // return fresh instance
     516        // Return fresh instance.
    483517        return $this->fresh();
    484518    }
    485519
    486520    /**
    487      * Create and get a model
    488      *
    489      * @param array $args
     521     * Create and get a model.
     522     *
     523     * @param array $args Attributes for the new model.
    490524     * @return Model|\WP_Error
    491525     */
     
    505539     * argument with the optional second array argument.
    506540     *
    507      * @param array $search Model to search for
    508      * @param array $create Attributes to create
     541     * @param array $search Model to search for.
     542     * @param array $update Attributes to update or create with.
    509543     * @return Model|\WP_Error
    510544     */
    511545    public function getOrCreate( $search, $update = array() ) {
    512         // look for model
     546        // Look for model.
    513547        $models = $this->fetch( $search );
    514548        if ( is_wp_error( $models ) ) {
     
    516550        }
    517551
    518         // already created, update it
     552        // Already created, return it.
    519553        if ( ! empty( $models->data[0] ) && ! empty( $update ) ) {
    520554            $this->set( $models->data[0]->toObject() );
     
    522556        }
    523557
    524         // merge and create
     558        // Merge and create.
    525559        $merged = array_merge( $search, $update );
    526560
    527         // unset query stuff
     561        // Unset query stuff.
    528562        if ( ! empty( $merged['date_query'] ) ) {
    529563            unset( $merged['date_query'] );
     
    532566        $this->create( $merged );
    533567
    534         // return fresh instance
     568        // Return fresh instance.
    535569        return $this->fresh();
    536570    }
     
    543577     * argument with the optional second array argument.
    544578     *
    545      * @param array $search Model to search for
    546      * @param array $create Attributes to create
     579     * @param array $search Model to search for.
     580     * @param array $update Attributes to update or create with.
    547581     * @return Model|\WP_Error
    548582     */
    549583    public function updateOrCreate( $search, $update = array() ) {
    550         // look for model
     584        // Look for model.
    551585        $models = $this->fetch( $search );
    552586        if ( is_wp_error( $models ) ) {
     
    554588        }
    555589
    556         // already created, update it
     590        // Already created, update it.
    557591        if ( ! empty( $models->data[0] ) && ! empty( $update ) ) {
    558592            $this->set( $models->data[0]->toObject() );
     
    561595        }
    562596
    563         // merge and create
     597        // Merge and create.
    564598        $merged = array_merge( $search, $update );
    565599
    566         // unset query stuff
     600        // Unset query stuff.
    567601        if ( ! empty( $merged['date_query'] ) ) {
    568602            unset( $merged['date_query'] );
     
    571605        $this->create( $merged );
    572606
    573         // return fresh instance
     607        // Return fresh instance.
    574608        return $this->fresh();
    575609    }
    576610
    577611    /**
    578      * Gets a single model
    579      *
    580      * @param int $id
    581      *
    582      * @return Model Model object
     612     * Gets a single model.
     613     *
     614     * @param int $id Model ID.
     615     *
     616     * @return Model Model object.
    583617     */
    584618    public function get( $id ) {
    585619        global $wpdb;
    586620
    587         // maybe cache results
     621        // Maybe cache results.
    588622        $results = $wpdb->get_row(
     623            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is a class property, not user input.
    589624            $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}{$this->table} WHERE id=%d", $id )
    590625        );
     
    607642
    608643    /**
    609      * Set attributes
    610      *
    611      * @param array $args
     644     * Set attributes.
     645     *
     646     * @param array|object $args Attributes to set.
    612647     * @return Model
    613648     */
    614649    public function set( $args ) {
     650        // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores -- Existing hook name, cannot change without breaking backwards compatibility.
    615651        $this->attributes = apply_filters( "presto_player/{$this->table}/data", $this->formatRow( $args ) );
    616652        return $this;
     
    618654
    619655    /**
    620      * Update a model
    621      *
    622      * @param array $args
     656     * Update a model.
     657     *
     658     * @param array $args Attributes to update.
    623659     * @return Model
    624660     */
     
    626662        global $wpdb;
    627663
    628         // id is required
     664        // ID is required.
    629665        if ( empty( $this->attributes->id ) ) {
    630666            return new \WP_Error( 'missing_parameter', __( 'You must first create or save this model to update it.', 'presto-player' ) );
    631667        }
    632668
    633         // unset guarded args
     669        // Unset guarded args.
    634670        $args = $this->unsetGuarded( $args );
    635671
    636         // parse args with default args
     672        // Parse args with default args.
    637673        $args = wp_parse_args( $args, $this->getDefaults() );
    638674
    639         // update time
     675        // Update time.
    640676        if ( ! empty( $this->schema()['updated_at'] ) ) {
    641677            $args['updated_at'] = ! empty( $args['updated_at'] ) ? $args['updated_at'] : current_time( 'mysql' );
    642678        }
    643679
    644         // maybe serialize
     680        // Maybe serialize.
    645681        $args = $this->maybeSerializeArgs( $args );
    646682
    647         // make update
     683        // Make update.
    648684        $updated = $wpdb->update( $wpdb->prefix . $this->table, $args, array( 'id' => (int) $this->id ) );
    649685
    650         // check for failure
     686        // Check for failure.
    651687        if ( false === $updated ) {
    652688            return new \WP_Error( 'update_failure', __( 'There was an issue updating the model.', 'presto-player' ) );
    653689        }
    654690
    655         // set attributes in model
     691        // Set attributes in model.
    656692        $this->set( $this->get( $this->id )->toObject() );
    657693
    658         // created action
     694        // Updated action.
    659695        do_action( "{$this->table}_updated", $this );
    660696
     
    663699
    664700    /**
    665      * Trash model
     701     * Trash model.
    666702     *
    667703     * @return Model
     
    672708
    673709    /**
    674      * Untrash model
     710     * Untrash model.
    675711     *
    676712     * @return Model
     
    681717
    682718    /**
    683      * Permanently delete model
    684      *
    685      * @return boolean Whether the model was deleted
     719     * Permanently delete model.
     720     *
     721     * @param array $where Where clause for deletion.
     722     * @return boolean Whether the model was deleted.
    686723     */
    687724    public function delete( $where = array() ) {
     
    694731
    695732    /**
    696      * Bulk delete by a list of ids
    697      *
    698      * @param array $ids
    699      * @return void
     733     * Bulk delete by a list of ids.
     734     *
     735     * @param array $ids Array of IDs to delete.
     736     * @return boolean Whether the records were deleted.
    700737     */
    701738    public function bulkDelete( $ids = array() ) {
    702739        global $wpdb;
    703740
    704         // convert to comman separated
    705         $ids = implode( ',', array_map( 'absint', $ids ) );
    706 
    707         // delete in bulk
     741        $ids = array_filter( array_map( 'absint', $ids ) );
     742        if ( empty( $ids ) ) {
     743            return false;
     744        }
     745
     746        $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
    708747        return (bool) $wpdb->query(
    709             $wpdb->prepare( "DELETE FROM {$wpdb->prefix}{$this->table} WHERE id IN(%1s)", $ids )
     748            $wpdb->prepare( "DELETE FROM {$wpdb->prefix}{$this->table} WHERE id IN($placeholders)", ...$ids ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table name is a class property. $placeholders is generated from array_fill with %d.
    710749        );
    711750    }
    712751
    713752    /**
    714      * Has One Relationship
     753     * Has One Relationship.
     754     *
     755     * @param string $classname    Related class name.
     756     * @param string $parent_field Parent field name.
     757     * @return HasOneRelationship
    715758     */
    716759    public function hasOne( $classname, $parent_field ) {
  • presto-player/tags/4.1.1/inc/Services/VideoPostType.php

    r3429192 r3484635  
    234234        if ( is_array( $tags ) ) {
    235235            foreach ( $tags as $key => $tag ) {
    236                 $tags[ $key ] = '<a href="?post_type=pp_video_block&pp_video_tag=' . $tag->term_id . '">' . $tag->name . '</a>';
     236                // Escape tag name to prevent XSS, use esc_url and absint for defense-in-depth.
     237                $tags[ $key ] = '<a href="' . esc_url( '?post_type=pp_video_block&pp_video_tag=' . absint( $tag->term_id ) ) . '">' . esc_html( $tag->name ) . '</a>';
    237238            }
    238             echo implode( ', ', $tags );
     239            // Each tag link is already individually escaped above.
     240            echo wp_kses_post( implode( ', ', $tags ) );
    239241        }
    240242        return ob_get_clean();
     
    461463        }
    462464
    463         $selected      = isset( $_GET[ $taxonomy ] ) ? $_GET[ $taxonomy ] : '';
     465        $selected      = isset( $_GET[ $taxonomy ] ) ? absint( $_GET[ $taxonomy ] ) : '';
    464466        $info_taxonomy = get_taxonomy( $taxonomy );
    465467
     468        ob_start();
    466469        wp_dropdown_categories(
    467470            array(
     
    475478            )
    476479        );
     480        $dropdown = ob_get_clean();
     481
     482        $allowed_html = array(
     483            'select' => array(
     484                'name'  => true,
     485                'id'    => true,
     486                'class' => true,
     487            ),
     488            'option' => array(
     489                'class'    => true,
     490                'value'    => true,
     491                'selected' => true,
     492            ),
     493        );
     494        echo wp_kses( $dropdown, $allowed_html );
    477495    }
    478496
  • presto-player/tags/4.1.1/languages/presto-player.pot

    r3468484 r3484635  
    88"Content-Type: text/plain; charset=UTF-8\n"
    99"Content-Transfer-Encoding: 8bit\n"
    10 "POT-Creation-Date: 2026-02-24T09:45:48+00:00\n"
     10"POT-Creation-Date: 2026-03-17T10:02:32+00:00\n"
    1111"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1212"X-Generator: WP-CLI 2.12.0\n"
     
    20232023
    20242024#: admin/blocks/shared/presets/parts/MailchimpConfig.js:75
    2025 #: Services/VideoPostType.php:389
     2025#: Services/VideoPostType.php:391
    20262026msgid "Tag"
    20272027msgstr ""
     
    24722472
    24732473#: admin/settings/pages/General.js:193
    2474 #: Services/VideoPostType.php:410
     2474#: Services/VideoPostType.php:412
    24752475msgid "Media Hub"
    24762476msgstr ""
     
    34053405
    34063406#: Integrations/Elementor/ReusableVideoWidget.php:126
    3407 #: Services/VideoPostType.php:408
     3407#: Services/VideoPostType.php:410
    34083408msgid "Edit Media"
    34093409msgstr ""
     
    35893589msgstr ""
    35903590
    3591 #: Models/Model.php:630
     3591#: Models/Model.php:666
    35923592msgid "You must first create or save this model to update it."
    35933593msgstr ""
    35943594
    3595 #: Models/Model.php:652
     3595#: Models/Model.php:688
    35963596msgid "There was an issue updating the model."
    35973597msgstr ""
     
    40264026msgstr ""
    40274027
    4028 #: Services/VideoPostType.php:322
     4028#: Services/VideoPostType.php:324
    40294029msgid "Enter a title..."
    40304030msgstr ""
    40314031
    4032 #: Services/VideoPostType.php:384
     4032#: Services/VideoPostType.php:386
    40334033msgctxt "post type general name"
    40344034msgid "Media Tags"
    40354035msgstr ""
    40364036
    4037 #: Services/VideoPostType.php:385
     4037#: Services/VideoPostType.php:387
    40384038msgctxt "post type singular name"
    40394039msgid "Media Tag"
    40404040msgstr ""
    40414041
    4042 #: Services/VideoPostType.php:386
     4042#: Services/VideoPostType.php:388
    40434043msgctxt "admin menu"
    40444044msgid "Search Media Tags"
    40454045msgstr ""
    40464046
    4047 #: Services/VideoPostType.php:387
     4047#: Services/VideoPostType.php:389
    40484048msgctxt "add new on admin bar"
    40494049msgid "Popular Media Tags"
    40504050msgstr ""
    40514051
    4052 #: Services/VideoPostType.php:401
     4052#: Services/VideoPostType.php:403
    40534053msgctxt "post type general name"
    40544054msgid "Media Hub"
    40554055msgstr ""
    40564056
    4057 #: Services/VideoPostType.php:402
     4057#: Services/VideoPostType.php:404
    40584058msgctxt "post type singular name"
    40594059msgid "Media"
    40604060msgstr ""
    40614061
    4062 #: Services/VideoPostType.php:403
     4062#: Services/VideoPostType.php:405
    40634063msgctxt "admin menu"
    40644064msgid "Media"
    40654065msgstr ""
    40664066
    4067 #: Services/VideoPostType.php:404
     4067#: Services/VideoPostType.php:406
    40684068msgctxt "add new on admin bar"
    40694069msgid "Presto Media"
    40704070msgstr ""
    40714071
    4072 #: Services/VideoPostType.php:405
     4072#: Services/VideoPostType.php:407
    40734073msgctxt "Media"
    40744074msgid "Add New"
    40754075msgstr ""
    40764076
    4077 #: Services/VideoPostType.php:406
     4077#: Services/VideoPostType.php:408
    40784078msgid "Add New Media"
    40794079msgstr ""
    40804080
    4081 #: Services/VideoPostType.php:407
     4081#: Services/VideoPostType.php:409
    40824082msgid "New Media"
    40834083msgstr ""
    40844084
    4085 #: Services/VideoPostType.php:409
     4085#: Services/VideoPostType.php:411
    40864086msgid "View Media"
    40874087msgstr ""
    40884088
    4089 #: Services/VideoPostType.php:411
     4089#: Services/VideoPostType.php:413
    40904090msgid "Search Media"
    40914091msgstr ""
    40924092
    4093 #: Services/VideoPostType.php:412
     4093#: Services/VideoPostType.php:414
    40944094msgid "No Media found."
    40954095msgstr ""
    40964096
    4097 #: Services/VideoPostType.php:413
     4097#: Services/VideoPostType.php:415
    40984098msgid "No Media found in Trash."
    40994099msgstr ""
    41004100
    4101 #: Services/VideoPostType.php:414
     4101#: Services/VideoPostType.php:416
    41024102msgid "Filter Media list"
    41034103msgstr ""
    41044104
    4105 #: Services/VideoPostType.php:415
     4105#: Services/VideoPostType.php:417
    41064106msgid "Media list navigation"
    41074107msgstr ""
    41084108
    4109 #: Services/VideoPostType.php:416
     4109#: Services/VideoPostType.php:418
    41104110msgid "Media list"
    41114111msgstr ""
    41124112
    4113 #: Services/VideoPostType.php:417
     4113#: Services/VideoPostType.php:419
    41144114msgid "Media published."
    41154115msgstr ""
    41164116
    4117 #: Services/VideoPostType.php:418
     4117#: Services/VideoPostType.php:420
    41184118msgid "Media published privately."
    41194119msgstr ""
    41204120
    4121 #: Services/VideoPostType.php:419
     4121#: Services/VideoPostType.php:421
    41224122msgid "Media reverted to draft."
    41234123msgstr ""
    41244124
    4125 #: Services/VideoPostType.php:420
     4125#: Services/VideoPostType.php:422
    41264126msgid "Media scheduled."
    41274127msgstr ""
    41284128
    4129 #: Services/VideoPostType.php:421
     4129#: Services/VideoPostType.php:423
    41304130msgid "Media updated."
    41314131msgstr ""
    41324132
    4133 #: Services/VideoPostType.php:468
     4133#: Services/VideoPostType.php:471
    41344134#, php-format
    41354135msgid "Show all %s"
  • presto-player/tags/4.1.1/presto-player.php

    r3468484 r3484635  
    44 * Plugin URI: http://prestoplayer.com
    55 * Description: A beautiful, fast media player for WordPress.
    6  * Version: 4.1.0
     6 * Version: 4.1.1
    77 * Author: Presto Made, Inc
    88 * Author URI: https://prestoplayer.com/
  • presto-player/tags/4.1.1/readme.txt

    r3468484 r3484635  
    55Requires at least: 6.3
    66Tested up to: 6.9
    7 Stable tag: 4.1.0
     7Stable tag: 4.1.1
    88Requires PHP: 7.3
    99License: GPLv2 or later
     
    157157== Changelog ==
    158158
     159= 4.1.1 =
     160* Security: Multiple security hardening improvements.
     161
    159162= 4.1.0 =
    160163* New: Automatic caption generation with BunnyCDN.
  • presto-player/tags/4.1.1/vendor/composer/installed.php

    r3468484 r3484635  
    22    'root' => array(
    33        'name' => 'course/player',
    4         'pretty_version' => 'v4.1.0',
    5         'version' => '4.1.0.0',
    6         'reference' => '2bd337ed2bb551577b4496ecd06fed90574dc6d4',
     4        'pretty_version' => 'v4.1.1',
     5        'version' => '4.1.1.0',
     6        'reference' => '788f19da99c0e35e11897dd50e9a1aae24c68e63',
    77        'type' => 'project',
    88        'install_path' => __DIR__ . '/../../',
     
    4848        ),
    4949        'course/player' => array(
    50             'pretty_version' => 'v4.1.0',
    51             'version' => '4.1.0.0',
    52             'reference' => '2bd337ed2bb551577b4496ecd06fed90574dc6d4',
     50            'pretty_version' => 'v4.1.1',
     51            'version' => '4.1.1.0',
     52            'reference' => '788f19da99c0e35e11897dd50e9a1aae24c68e63',
    5353            'type' => 'project',
    5454            'install_path' => __DIR__ . '/../../',
  • presto-player/trunk/dist/components/stats.json

    r3468484 r3484635  
    11{
    2   "timestamp": "2026-02-24T09:46:04",
     2  "timestamp": "2026-03-17T10:02:49",
    33  "compiler": {
    44    "name": "node",
    5     "version": "20.20.0"
     5    "version": "20.20.1"
    66  },
    77  "app": {
  • presto-player/trunk/inc/Factory.php

    r3468484 r3484635  
    88use PrestoPlayer\Support\Block;
    99use PrestoPlayer\Services\Scripts;
    10 use PrestoPlayer\Services\BunnyCDN;
     10// Disabled: PrestoPlayer\Services\BunnyCDN class does not exist. Kept for reference.
     11// use PrestoPlayer\Services\BunnyCDN;
    1112use PrestoPlayer\Services\Settings;
    1213use PrestoPlayer\Services\AdminNotices;
     
    3637    public function getRules() {
    3738        return array(
    38             BunnyCDN::class       => self::SHARED,
     39            // Disabled: BunnyCDN service class does not exist; this was a stale reference.
     40            // BunnyCDN::class       => self::SHARED,
    3941            Visits::class         => self::SHARED,
    4042            ReusableVideos::class => self::SHARED,
  • presto-player/trunk/inc/Integrations/LearnDash/LearnDash.php

    r3317175 r3484635  
    4141        ob_start();
    4242        ?>
    43         data-video-cookie-key="<?php echo $data['cookieKey']; ?>"
    44         data-video-progression="<?php echo $data['videoProgress']; ?>"
     43        data-video-cookie-key="<?php echo esc_attr( $data['cookieKey'] ); ?>"
     44        data-video-progression="<?php echo esc_attr( $data['videoProgress'] ); ?>"
    4545        data-video-provider="presto"
    4646        <?php
  • presto-player/trunk/inc/Libraries/BunnyCDN.php

    r3122484 r3484635  
    1 <?php
     1<?php // phpcs:ignoreFile -- Third-party BunnyCDN SDK library; WPCS enforcement would require full rewrite.
    22
    33namespace PrestoPlayer\Libraries;
    44
     5/**
     6 * Third-party BunnyCDN SDK library.
     7 *
     8 * NOTE: This file is NOT actively used — no code in either the free (Presto Player)
     9 * or pro (Presto Player Pro) plugin instantiates or calls this class.
     10 * It is kept here for future reference only.
     11 *
     12 * @see https://github.com/prestomade/presto-player/issues/898
     13 * @see https://github.com/prestomade/presto-player/issues/899
     14 */
    515class BunnyCDN {
    616
     
    12151225        $file = $api_call['data'];
    12161226
    1217         header( 'Content-type: application/octet-stream' );
    1218         header( "Content-Disposition: attachment; filename=$file_name" );
    1219         // will force to download...
     1227        $safe_name = str_replace( '"', '', sanitize_file_name( $file_name ) );
     1228        header( 'Content-Type: application/octet-stream' );
     1229        header( 'Content-Disposition: attachment; filename="' . $safe_name . '"' );
    12201230        echo $file;
    12211231    }
     
    12981308
    12991309    public function DownloadFile( $file_url = '', $oupt_file_name = '' ) {
    1300         // this is a fast way to download a file
    1301         // remove any query string data
    1302         if ( isset( $oupt_file_name ) ) {
     1310        $parsed = wp_parse_url( $file_url );
     1311        if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], array( 'http', 'https' ), true ) ) {
     1312            wp_die( 'Invalid file URL.', 'Error', array( 'response' => 400 ) );
     1313        }
     1314
     1315        if ( ! empty( $oupt_file_name ) ) {
    13031316            $file_name = $oupt_file_name;
    1304         }
    1305         if ( empty( $oupt_file_name ) ) {
     1317        } else {
    13061318            $file_name = preg_replace( '/\?.*/', '', basename( $file_url ) );
     1319        }
     1320
     1321        $safe_name = str_replace( '"', '', sanitize_file_name( $file_name ) );
     1322
     1323        $response = wp_safe_remote_get( $file_url, array( 'timeout' => 60 ) );
     1324        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
     1325            wp_die( 'Download failed.', 'Error', array( 'response' => 500 ) );
    13071326        }
    13081327
    13091328        header( 'Content-Type: application/octet-stream' );
    13101329        header( 'Content-Transfer-Encoding: Binary' );
    1311         header( "Content-disposition: attachment; filename=$file_name" );
    1312         readfile( $file_url );
     1330        header( 'Content-Disposition: attachment; filename="' . $safe_name . '"' );
     1331        echo wp_remote_retrieve_body( $response );
     1332        exit;
    13131333    }
    13141334
    13151335
    13161336    public function DownloadFile1( $file_url ) {
    1317         /*
    1318             this is a slow way to download a file
    1319             will allow you to download a remote file from any server that is accessible
    1320         */
    1321 
    1322         $filename = $file_url;
    1323         $filedata = @file_get_contents( $filename );
    1324 
    1325         // SUCCESS
    1326         if ( $filedata ) {
    1327             // GET A NAME FOR THE FILE
    1328             // remove any query string data
    1329             $basename = preg_replace( '/\?.*/', '', basename( $file_url ) );
    1330             // $basename = basename($filename);
    1331 
    1332             // THESE HEADERS ARE USED ON ALL BROWSERS
    1333             header( 'Content-Type: application-x/force-download' );
    1334             header( "Content-Disposition: attachment; filename=$basename" );
    1335             header( 'Content-length: ' . (string) ( strlen( $filedata ) ) );
    1336             header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', mktime( date( 'H' ) + 2, date( 'i' ), date( 's' ), date( 'm' ), date( 'd' ), date( 'Y' ) ) ) . ' GMT' );
    1337             header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
    1338 
    1339             // THIS HEADER MUST BE OMITTED FOR IE 6+
    1340             if ( false === strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE ' ) ) {
    1341                 header( 'Cache-Control: no-cache, must-revalidate' );
    1342             }
    1343 
    1344             // THIS IS THE LAST HEADER
    1345             header( 'Pragma: no-cache' );
    1346 
    1347             // FLUSH THE HEADERS TO THE BROWSER
    1348             flush();
    1349 
    1350             // CAPTURE THE FILE IN THE OUTPUT BUFFERS - WILL BE FLUSHED AT SCRIPT END
    1351             ob_start();
    1352             echo $filedata;
    1353         }
    1354 
    1355         // FAILURE
    1356         else {
    1357             die( "ERROR: UNABLE TO OPEN $filename" );
    1358         }
     1337        $parsed = wp_parse_url( $file_url );
     1338        if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], array( 'http', 'https' ), true ) ) {
     1339            wp_die( 'Invalid file URL.', 'Error', array( 'response' => 400 ) );
     1340        }
     1341
     1342        $response = wp_safe_remote_get( $file_url, array( 'timeout' => 60 ) );
     1343        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
     1344            wp_die( 'Download failed.', 'Error', array( 'response' => 500 ) );
     1345        }
     1346
     1347        $filedata = wp_remote_retrieve_body( $response );
     1348        $basename = str_replace( '"', '', sanitize_file_name( preg_replace( '/\?.*/', '', basename( $file_url ) ) ) );
     1349
     1350        header( 'Content-Type: application/octet-stream' );
     1351        header( 'Content-Disposition: attachment; filename="' . $basename . '"' );
     1352        header( 'Content-Length: ' . strlen( $filedata ) );
     1353        header( 'Cache-Control: no-cache, must-revalidate' );
     1354        header( 'Pragma: no-cache' );
     1355
     1356        echo $filedata;
     1357        exit;
    13591358    }
    13601359
  • presto-player/trunk/inc/Models/Model.php

    r3122484 r3484635  
    11<?php
     2/**
     3 * Base Model class for interfacing with custom database tables.
     4 *
     5 * @package PrestoPlayer\Models
     6 */
    27
    38namespace PrestoPlayer\Models;
     
    712
    813/**
    9  * Model for interfacing with custom database tables
     14 * Model for interfacing with custom database tables.
    1015 */
    1116abstract class Model implements ModelInterface {
    1217
    1318    /**
    14      * Needs a table name
     19     * Needs a table name.
    1520     *
    1621     * @var string
     
    1924
    2025    /**
    21      * Store model attributes
     26     * Store model attributes.
    2227     *
    2328     * @var object
     
    2631
    2732    /**
    28      * Model schema
     33     * Model schema.
    2934     *
    3035     * @return array
     
    3540
    3641    /**
    37      * Guarded variables
     42     * Guarded variables.
    3843     *
    3944     * @var array
     
    4247
    4348    /**
    44      * Attributes we can query by
     49     * Attributes we can query by.
    4550     *
    4651     * @var array
     
    4954
    5055    /**
    51      * Optionally get something from the db
    52      *
    53      * @param integer $id
     56     * Optionally get something from the db.
     57     *
     58     * @param integer $id Model ID.
    5459     */
    5560    public function __construct( $id = 0 ) {
    5661        $this->attributes = new \stdClass();
    5762        if ( ! empty( $id ) ) {
    58             return $this->set( $this->get( $id )->toObject() );
    59             return $this;
    60         }
    61         return $this;
    62     }
    63 
    64     /**
    65      * Get attributes properties
    66      *
    67      * @param string $property
     63            $this->set( $this->get( $id )->toObject() );
     64        }
     65    }
     66
     67    /**
     68     * Get attributes properties.
     69     *
     70     * @param string $property Property name.
    6871     * @return mixed
    6972     */
     
    7578
    7679    /**
    77      * Get attributes properties
    78      *
    79      * @param string $property
    80      * @return mixed
     80     * Set attributes properties.
     81     *
     82     * @param string $property Property name.
     83     * @param mixed  $value    Property value.
     84     * @return void
    8185     */
    8286    public function __set( $property, $value ) {
     
    8488    }
    8589
     90    /**
     91     * Get the table name.
     92     *
     93     * @return string
     94     */
    8695    public function getTableName() {
    8796        return $this->table;
     
    8998
    9099    /**
    91      * Convert to Object
     100     * Convert to Object.
    92101     *
    93102     * @return object
     
    106115
    107116    /**
    108      * Convert to array
     117     * Convert to array.
    109118     *
    110119     * @return array
     
    115124
    116125    /**
    117      * Formats row data based on schema
    118      *
    119      * @param object $columns
     126     * Formats row data based on schema.
     127     *
     128     * @param object $columns Row columns to format.
    120129     * @return object
    121130     */
     
    136145
    137146    /**
    138      * Fetch all models
    139      *
    140      * @return array Array of preset objects
     147     * Fetch all models.
     148     *
     149     * @return array Array of preset objects.
    141150     */
    142151    public function all() {
    143152        global $wpdb;
    144153
    145         // maybe get only published if we have soft deletes
     154        // Maybe get only published if we have soft deletes.
    146155        $where = ! empty( $this->schema()['deleted_at'] ) ? "WHERE (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00') " : '';
    147156
    148157        $results = $wpdb->get_results(
    149             "SELECT * FROM {$wpdb->prefix}{$this->table} $where"
     158            "SELECT * FROM {$wpdb->prefix}{$this->table} $where" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is a class property. $where is built from safe values.
    150159        );
    151160
     
    154163
    155164    /**
    156      * Fetch models from db
    157      *
    158      * @param array $args
    159      * @return Object Array of models with pagination data
     165     * Fetch models from db.
     166     *
     167     * @param array $args Query arguments.
     168     * @return object|\WP_Error Array of models with pagination data.
    160169     */
    161170    public function fetch( $args = array() ) {
    162171        global $wpdb;
    163172
    164         // remove empties for querying
     173        // Remove empties for querying.
    165174        $args = array_filter(
    166175            wp_parse_args(
     
    175184        );
    176185
    177         // get query args
     186        // Get query args.
    178187        $query = array_filter(
    179188            $args,
    180189            function ( $key ) {
    181                 return in_array( $key, array( 'per_page', 'page', 'status', 'date_query', 'fields', 'order_by' ) );
     190                return in_array( $key, array( 'per_page', 'page', 'status', 'date_query', 'fields', 'order_by' ), true );
    182191            },
    183192            ARRAY_FILTER_USE_KEY
     
    188197
    189198        foreach ( $args as $attribute => $value ) {
    190             // must be queryable and in schema
    191             if ( ! in_array( $attribute, $this->queryable ) || empty( $schema[ $attribute ] ) ) {
     199            // Must be queryable and in schema.
     200            if ( ! in_array( $attribute, $this->queryable, true ) || empty( $schema[ $attribute ] ) ) {
    192201                unset( $args[ $attribute ] );
    193202                continue;
    194203            }
    195204
    196             // attribute schema
     205            // Attribute schema.
    197206            $attr_schema = $schema[ $attribute ];
    198207
    199             // force type
     208            // Force type.
    200209            settype( $value, $attr_schema['type'] );
    201210
    202             // sanitize input
     211            // Sanitize input.
    203212            if ( ! empty( $attr_schema['sanitize_callback'] ) ) {
    204213                $value = $attr_schema['sanitize_callback']( $value );
    205214            }
    206215
    207             // maybe add quotes
    208             if ( in_array( $attr_schema['type'], array( 'integer', 'number', 'boolean' ) ) ) {
    209                 $where .= $wpdb->prepare( 'AND %1s=%2s ', $attribute, $value );
     216            // Column name is already validated against $this->queryable whitelist above.
     217            if ( in_array( $attr_schema['type'], array( 'integer', 'number', 'boolean' ), true ) ) {
     218                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $attribute is validated against queryable whitelist.
     219                $where .= $wpdb->prepare( "AND `{$attribute}` = %d ", $value );
    210220            } else {
    211                 $where .= $wpdb->prepare( "AND %1s='%2s' ", $attribute, $value );
    212             }
    213         }
    214 
    215         // soft deletes
     221                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $attribute is validated against queryable whitelist.
     222                $where .= $wpdb->prepare( "AND `{$attribute}` = %s ", $value );
     223            }
     224        }
     225
     226        // Soft deletes.
    216227        if ( ! empty( $this->schema()['deleted_at'] ) ) {
    217228            $status = ! empty( $args['status'] ) ? $args['status'] : '';
     
    220231                    $where .= "AND (deleted_at IS NOT NULL OR deleted_at != '0000-00-00 00:00:00') ";
    221232                    break;
    222                 default: // default to published
     233                default: // Default to published.
    223234                    $where .= "AND (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00') ";
    224235                    break;
     
    226237        }
    227238
    228         // before and after
     239        // Before and after date queries.
    229240        if ( ! empty( $query['date_query'] ) ) {
    230             // use created at by default
     241            // Use created_at by default.
    231242            $query['date_query'] = wp_parse_args(
    232243                $query['date_query'],
     
    236247            );
    237248
    238             // check for field
    239             $field = ! empty( $this->schema()[ $query['date_query']['field'] ] ) ? sanitize_text_field( $query['date_query']['field'] ) : null;
     249            // Check for valid field.
     250            $field = ! empty( $this->schema()[ $query['date_query']['field'] ] ) ? $query['date_query']['field'] : null;
    240251            if ( ! $field ) {
    241252                return new \WP_Error( 'invalid_field', 'Cannot do a date query by ' . sanitize_text_field( $query['date_query']['field'] ) );
    242253            }
    243254
    244             // if after
     255            // Field name is validated against schema above.
    245256            if ( ! empty( $query['date_query']['after'] ) ) {
    246257                $where .= $wpdb->prepare(
    247                     "AND %1s >= '%2s' ",
    248                     sanitize_text_field( $field ), // i.e. created_at
    249                     date( 'Y-m-d H:i:s', strtotime( $query['date_query']['after'] ) ) // convert to date
     258                    "AND `{$field}` >= %s ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $field is validated against schema whitelist.
     259                    gmdate( 'Y-m-d H:i:s', strtotime( $query['date_query']['after'] ) )
    250260                );
    251261            }
    252             // before
    253262            if ( ! empty( $query['date_query']['before'] ) ) {
    254263                $where .= $wpdb->prepare(
    255                     "AND %1s <= '%2s' ",
    256                     sanitize_text_field( $field ), // i.e. created_at
    257                     date( 'Y-m-d H:i:s', strtotime( $query['date_query']['before'] ) ) // convert to date
     264                    "AND `{$field}` <= %s ", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $field is validated against schema whitelist.
     265                    gmdate( 'Y-m-d H:i:s', strtotime( $query['date_query']['before'] ) )
    258266                );
    259267            }
     
    262270        $limit      = (int) $query['per_page'];
    263271        $offset     = (int) ( $query['per_page'] * ( $query['page'] - 1 ) );
    264         $pagination = $wpdb->prepare( 'LIMIT %1s OFFSET %2s ', $limit, $offset );
     272        $pagination = $wpdb->prepare( 'LIMIT %d OFFSET %d ', $limit, $offset );
    265273
    266274        $select = '*';
     
    271279        $order_by = '';
    272280        if ( ! empty( $query['order_by'] ) ) {
    273             $order_by .= 'ORDER BY';
    274             $number    = count( $query['order_by'] );
    275             $i         = 1;
     281            $allowed_dirs  = array( 'ASC', 'DESC' );
     282            $order_clauses = array();
    276283            foreach ( $query['order_by'] as $attribute => $direction ) {
    277                 $order_by .= $wpdb->prepare( ' %1s %2s', $attribute, $direction );
    278                 $order_by .= $i === $number ? '' : ',';
    279                 ++$i;
    280             }
    281             $order_by .= ' ';
    282         }
    283 
    284         $total   = $wpdb->get_var( "SELECT count(id) as count FROM {$wpdb->prefix}{$this->table} $where$order_by" );
     284                if ( ! in_array( $attribute, $this->queryable, true ) && ! array_key_exists( $attribute, $schema ) ) {
     285                    continue;
     286                }
     287                $direction       = in_array( strtoupper( $direction ), $allowed_dirs, true ) ? strtoupper( $direction ) : 'ASC';
     288                $order_clauses[] = "`{$attribute}` {$direction}";
     289            }
     290            if ( ! empty( $order_clauses ) ) {
     291                $order_by = 'ORDER BY ' . implode( ', ', $order_clauses ) . ' ';
     292            }
     293        }
     294
     295        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $where, $order_by, $pagination are built from safe values above.
     296        $total = $wpdb->get_var( "SELECT count(id) as count FROM {$wpdb->prefix}{$this->table} $where" );
     297        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $select, $where, $order_by, $pagination are built from safe values above.
    285298        $results = $wpdb->get_results( "SELECT $select FROM {$wpdb->prefix}{$this->table} $where$order_by$pagination" );
    286299
     
    294307
    295308    /**
    296      * Find a specific model based on query
     309     * Find a specific model based on query.
     310     *
     311     * @param array $args Query arguments.
     312     * @return Model|false
    297313     */
    298314    public function findWhere( $args = array() ) {
     
    303319
    304320    /**
    305      * Turns raw sql query results into models
    306      *
    307      * @param array $results
    308      * @return array Array of Models
     321     * Turns raw sql query results into models.
     322     *
     323     * @param array $results Raw database results.
     324     * @return array Array of Models.
    309325     */
    310326    protected function parseResults( $results ) {
     
    317333
    318334        $output = array();
    319         // return new models for each row
     335        // Return new models for each row.
    320336        foreach ( $results as $result ) {
    321337            $class    = get_class( $this );
     
    326342    }
    327343
     344    /**
     345     * Parse results into an array of IDs.
     346     *
     347     * @param array $results Raw database results.
     348     * @return array Array of integer IDs.
     349     */
    328350    public function parseIds( $results ) {
    329351        if ( is_wp_error( $results ) ) {
     
    342364
    343365    /**
    344      * Gets fresh data from the db
     366     * Gets fresh data from the db.
    345367     *
    346368     * @return Model
     
    354376
    355377    /**
    356      * Get default values set from scheam
     378     * Get default values set from schema.
    357379     *
    358380     * @return array
     
    372394
    373395    /**
    374      * Unset guarded variables
    375      *
    376      * @param array $args
    377      * @return void
     396     * Unset guarded variables.
     397     *
     398     * @param array $args Arguments to filter.
     399     * @return array
    378400     */
    379401    protected function unsetGuarded( $args = array() ) {
    380         // unset guarded
     402        // Unset guarded.
    381403        foreach ( $this->guarded as $arg ) {
    382404            if ( $args[ $arg ] ) {
     
    385407        }
    386408
    387         // we should never set an ID
     409        // We should never set an ID.
    388410        unset( $args['id'] );
    389411
     
    392414
    393415    /**
    394      * Create a preset
    395      *
    396      * @param array $args
     416     * Create a model.
     417     *
     418     * @param array $args Attributes for the new model.
    397419     * @return integer
    398420     */
     
    400422        global $wpdb;
    401423
    402         // unset guarded args
     424        // Unset guarded args.
    403425        $args = $this->unsetGuarded( $args );
    404426
    405         // parse args with default args
     427        // Parse args with default args.
    406428        $args = wp_parse_args( $args, $this->getDefaults() );
    407429
    408         // creation time
     430        // Creation time.
    409431        if ( ! empty( $this->schema()['created_at'] ) ) {
    410432            $args['created_at'] = ! empty( $args['created_at'] ) ? $args['created_at'] : current_time( 'mysql' );
    411433        }
    412434
    413         // maybe serialize args
     435        // Maybe serialize args.
    414436        $args = $this->maybeSerializeArgs( $args );
    415437
    416         // insert
     438        // Insert.
    417439        $wpdb->insert( $wpdb->prefix . $this->table, $args );
    418440
    419         // set ID in attributes
     441        // Set ID in attributes.
    420442        $this->attributes->id = $wpdb->insert_id;
    421443
    422         // created action
     444        // Created action.
    423445        do_action( "{$this->table}_created", $this );
    424446
    425         // return id
     447        // Return id.
    426448        return $this->attributes->id;
    427449    }
    428450
     451    /**
     452     * Maybe serialize array arguments.
     453     *
     454     * @param array $args Arguments to process.
     455     * @return array
     456     */
    429457    protected function maybeSerializeArgs( $args ) {
    430458        foreach ( $args as $key => $arg ) {
     
    438466    }
    439467
     468    /**
     469     * Maybe unserialize array arguments.
     470     *
     471     * @param array $args Arguments to process.
     472     * @return array
     473     */
    440474    protected function maybeUnSerializeArgs( $args ) {
    441475        foreach ( $args as $key => $arg ) {
     
    456490     * argument with the optional second array argument.
    457491     *
    458      * @param array $search Model to search for
    459      * @param array $create Attributes to create
     492     * @param array $search Model to search for.
     493     * @param array $create Attributes to create.
    460494     * @return Model|\WP_Error
    461495     */
     
    470504        }
    471505
    472         // already created
     506        // Already created.
    473507        if ( ! empty( $models->data[0] ) ) {
    474508            $this->set( $models->data[0]->toObject() );
     
    476510        }
    477511
    478         // merge and create
     512        // Merge and create.
    479513        $merged = array_merge( $search, $create );
    480514        $this->create( $merged );
    481515
    482         // return fresh instance
     516        // Return fresh instance.
    483517        return $this->fresh();
    484518    }
    485519
    486520    /**
    487      * Create and get a model
    488      *
    489      * @param array $args
     521     * Create and get a model.
     522     *
     523     * @param array $args Attributes for the new model.
    490524     * @return Model|\WP_Error
    491525     */
     
    505539     * argument with the optional second array argument.
    506540     *
    507      * @param array $search Model to search for
    508      * @param array $create Attributes to create
     541     * @param array $search Model to search for.
     542     * @param array $update Attributes to update or create with.
    509543     * @return Model|\WP_Error
    510544     */
    511545    public function getOrCreate( $search, $update = array() ) {
    512         // look for model
     546        // Look for model.
    513547        $models = $this->fetch( $search );
    514548        if ( is_wp_error( $models ) ) {
     
    516550        }
    517551
    518         // already created, update it
     552        // Already created, return it.
    519553        if ( ! empty( $models->data[0] ) && ! empty( $update ) ) {
    520554            $this->set( $models->data[0]->toObject() );
     
    522556        }
    523557
    524         // merge and create
     558        // Merge and create.
    525559        $merged = array_merge( $search, $update );
    526560
    527         // unset query stuff
     561        // Unset query stuff.
    528562        if ( ! empty( $merged['date_query'] ) ) {
    529563            unset( $merged['date_query'] );
     
    532566        $this->create( $merged );
    533567
    534         // return fresh instance
     568        // Return fresh instance.
    535569        return $this->fresh();
    536570    }
     
    543577     * argument with the optional second array argument.
    544578     *
    545      * @param array $search Model to search for
    546      * @param array $create Attributes to create
     579     * @param array $search Model to search for.
     580     * @param array $update Attributes to update or create with.
    547581     * @return Model|\WP_Error
    548582     */
    549583    public function updateOrCreate( $search, $update = array() ) {
    550         // look for model
     584        // Look for model.
    551585        $models = $this->fetch( $search );
    552586        if ( is_wp_error( $models ) ) {
     
    554588        }
    555589
    556         // already created, update it
     590        // Already created, update it.
    557591        if ( ! empty( $models->data[0] ) && ! empty( $update ) ) {
    558592            $this->set( $models->data[0]->toObject() );
     
    561595        }
    562596
    563         // merge and create
     597        // Merge and create.
    564598        $merged = array_merge( $search, $update );
    565599
    566         // unset query stuff
     600        // Unset query stuff.
    567601        if ( ! empty( $merged['date_query'] ) ) {
    568602            unset( $merged['date_query'] );
     
    571605        $this->create( $merged );
    572606
    573         // return fresh instance
     607        // Return fresh instance.
    574608        return $this->fresh();
    575609    }
    576610
    577611    /**
    578      * Gets a single model
    579      *
    580      * @param int $id
    581      *
    582      * @return Model Model object
     612     * Gets a single model.
     613     *
     614     * @param int $id Model ID.
     615     *
     616     * @return Model Model object.
    583617     */
    584618    public function get( $id ) {
    585619        global $wpdb;
    586620
    587         // maybe cache results
     621        // Maybe cache results.
    588622        $results = $wpdb->get_row(
     623            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is a class property, not user input.
    589624            $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}{$this->table} WHERE id=%d", $id )
    590625        );
     
    607642
    608643    /**
    609      * Set attributes
    610      *
    611      * @param array $args
     644     * Set attributes.
     645     *
     646     * @param array|object $args Attributes to set.
    612647     * @return Model
    613648     */
    614649    public function set( $args ) {
     650        // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores -- Existing hook name, cannot change without breaking backwards compatibility.
    615651        $this->attributes = apply_filters( "presto_player/{$this->table}/data", $this->formatRow( $args ) );
    616652        return $this;
     
    618654
    619655    /**
    620      * Update a model
    621      *
    622      * @param array $args
     656     * Update a model.
     657     *
     658     * @param array $args Attributes to update.
    623659     * @return Model
    624660     */
     
    626662        global $wpdb;
    627663
    628         // id is required
     664        // ID is required.
    629665        if ( empty( $this->attributes->id ) ) {
    630666            return new \WP_Error( 'missing_parameter', __( 'You must first create or save this model to update it.', 'presto-player' ) );
    631667        }
    632668
    633         // unset guarded args
     669        // Unset guarded args.
    634670        $args = $this->unsetGuarded( $args );
    635671
    636         // parse args with default args
     672        // Parse args with default args.
    637673        $args = wp_parse_args( $args, $this->getDefaults() );
    638674
    639         // update time
     675        // Update time.
    640676        if ( ! empty( $this->schema()['updated_at'] ) ) {
    641677            $args['updated_at'] = ! empty( $args['updated_at'] ) ? $args['updated_at'] : current_time( 'mysql' );
    642678        }
    643679
    644         // maybe serialize
     680        // Maybe serialize.
    645681        $args = $this->maybeSerializeArgs( $args );
    646682
    647         // make update
     683        // Make update.
    648684        $updated = $wpdb->update( $wpdb->prefix . $this->table, $args, array( 'id' => (int) $this->id ) );
    649685
    650         // check for failure
     686        // Check for failure.
    651687        if ( false === $updated ) {
    652688            return new \WP_Error( 'update_failure', __( 'There was an issue updating the model.', 'presto-player' ) );
    653689        }
    654690
    655         // set attributes in model
     691        // Set attributes in model.
    656692        $this->set( $this->get( $this->id )->toObject() );
    657693
    658         // created action
     694        // Updated action.
    659695        do_action( "{$this->table}_updated", $this );
    660696
     
    663699
    664700    /**
    665      * Trash model
     701     * Trash model.
    666702     *
    667703     * @return Model
     
    672708
    673709    /**
    674      * Untrash model
     710     * Untrash model.
    675711     *
    676712     * @return Model
     
    681717
    682718    /**
    683      * Permanently delete model
    684      *
    685      * @return boolean Whether the model was deleted
     719     * Permanently delete model.
     720     *
     721     * @param array $where Where clause for deletion.
     722     * @return boolean Whether the model was deleted.
    686723     */
    687724    public function delete( $where = array() ) {
     
    694731
    695732    /**
    696      * Bulk delete by a list of ids
    697      *
    698      * @param array $ids
    699      * @return void
     733     * Bulk delete by a list of ids.
     734     *
     735     * @param array $ids Array of IDs to delete.
     736     * @return boolean Whether the records were deleted.
    700737     */
    701738    public function bulkDelete( $ids = array() ) {
    702739        global $wpdb;
    703740
    704         // convert to comman separated
    705         $ids = implode( ',', array_map( 'absint', $ids ) );
    706 
    707         // delete in bulk
     741        $ids = array_filter( array_map( 'absint', $ids ) );
     742        if ( empty( $ids ) ) {
     743            return false;
     744        }
     745
     746        $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
    708747        return (bool) $wpdb->query(
    709             $wpdb->prepare( "DELETE FROM {$wpdb->prefix}{$this->table} WHERE id IN(%1s)", $ids )
     748            $wpdb->prepare( "DELETE FROM {$wpdb->prefix}{$this->table} WHERE id IN($placeholders)", ...$ids ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table name is a class property. $placeholders is generated from array_fill with %d.
    710749        );
    711750    }
    712751
    713752    /**
    714      * Has One Relationship
     753     * Has One Relationship.
     754     *
     755     * @param string $classname    Related class name.
     756     * @param string $parent_field Parent field name.
     757     * @return HasOneRelationship
    715758     */
    716759    public function hasOne( $classname, $parent_field ) {
  • presto-player/trunk/inc/Services/VideoPostType.php

    r3429192 r3484635  
    234234        if ( is_array( $tags ) ) {
    235235            foreach ( $tags as $key => $tag ) {
    236                 $tags[ $key ] = '<a href="?post_type=pp_video_block&pp_video_tag=' . $tag->term_id . '">' . $tag->name . '</a>';
     236                // Escape tag name to prevent XSS, use esc_url and absint for defense-in-depth.
     237                $tags[ $key ] = '<a href="' . esc_url( '?post_type=pp_video_block&pp_video_tag=' . absint( $tag->term_id ) ) . '">' . esc_html( $tag->name ) . '</a>';
    237238            }
    238             echo implode( ', ', $tags );
     239            // Each tag link is already individually escaped above.
     240            echo wp_kses_post( implode( ', ', $tags ) );
    239241        }
    240242        return ob_get_clean();
     
    461463        }
    462464
    463         $selected      = isset( $_GET[ $taxonomy ] ) ? $_GET[ $taxonomy ] : '';
     465        $selected      = isset( $_GET[ $taxonomy ] ) ? absint( $_GET[ $taxonomy ] ) : '';
    464466        $info_taxonomy = get_taxonomy( $taxonomy );
    465467
     468        ob_start();
    466469        wp_dropdown_categories(
    467470            array(
     
    475478            )
    476479        );
     480        $dropdown = ob_get_clean();
     481
     482        $allowed_html = array(
     483            'select' => array(
     484                'name'  => true,
     485                'id'    => true,
     486                'class' => true,
     487            ),
     488            'option' => array(
     489                'class'    => true,
     490                'value'    => true,
     491                'selected' => true,
     492            ),
     493        );
     494        echo wp_kses( $dropdown, $allowed_html );
    477495    }
    478496
  • presto-player/trunk/languages/presto-player.pot

    r3468484 r3484635  
    88"Content-Type: text/plain; charset=UTF-8\n"
    99"Content-Transfer-Encoding: 8bit\n"
    10 "POT-Creation-Date: 2026-02-24T09:45:48+00:00\n"
     10"POT-Creation-Date: 2026-03-17T10:02:32+00:00\n"
    1111"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1212"X-Generator: WP-CLI 2.12.0\n"
     
    20232023
    20242024#: admin/blocks/shared/presets/parts/MailchimpConfig.js:75
    2025 #: Services/VideoPostType.php:389
     2025#: Services/VideoPostType.php:391
    20262026msgid "Tag"
    20272027msgstr ""
     
    24722472
    24732473#: admin/settings/pages/General.js:193
    2474 #: Services/VideoPostType.php:410
     2474#: Services/VideoPostType.php:412
    24752475msgid "Media Hub"
    24762476msgstr ""
     
    34053405
    34063406#: Integrations/Elementor/ReusableVideoWidget.php:126
    3407 #: Services/VideoPostType.php:408
     3407#: Services/VideoPostType.php:410
    34083408msgid "Edit Media"
    34093409msgstr ""
     
    35893589msgstr ""
    35903590
    3591 #: Models/Model.php:630
     3591#: Models/Model.php:666
    35923592msgid "You must first create or save this model to update it."
    35933593msgstr ""
    35943594
    3595 #: Models/Model.php:652
     3595#: Models/Model.php:688
    35963596msgid "There was an issue updating the model."
    35973597msgstr ""
     
    40264026msgstr ""
    40274027
    4028 #: Services/VideoPostType.php:322
     4028#: Services/VideoPostType.php:324
    40294029msgid "Enter a title..."
    40304030msgstr ""
    40314031
    4032 #: Services/VideoPostType.php:384
     4032#: Services/VideoPostType.php:386
    40334033msgctxt "post type general name"
    40344034msgid "Media Tags"
    40354035msgstr ""
    40364036
    4037 #: Services/VideoPostType.php:385
     4037#: Services/VideoPostType.php:387
    40384038msgctxt "post type singular name"
    40394039msgid "Media Tag"
    40404040msgstr ""
    40414041
    4042 #: Services/VideoPostType.php:386
     4042#: Services/VideoPostType.php:388
    40434043msgctxt "admin menu"
    40444044msgid "Search Media Tags"
    40454045msgstr ""
    40464046
    4047 #: Services/VideoPostType.php:387
     4047#: Services/VideoPostType.php:389
    40484048msgctxt "add new on admin bar"
    40494049msgid "Popular Media Tags"
    40504050msgstr ""
    40514051
    4052 #: Services/VideoPostType.php:401
     4052#: Services/VideoPostType.php:403
    40534053msgctxt "post type general name"
    40544054msgid "Media Hub"
    40554055msgstr ""
    40564056
    4057 #: Services/VideoPostType.php:402
     4057#: Services/VideoPostType.php:404
    40584058msgctxt "post type singular name"
    40594059msgid "Media"
    40604060msgstr ""
    40614061
    4062 #: Services/VideoPostType.php:403
     4062#: Services/VideoPostType.php:405
    40634063msgctxt "admin menu"
    40644064msgid "Media"
    40654065msgstr ""
    40664066
    4067 #: Services/VideoPostType.php:404
     4067#: Services/VideoPostType.php:406
    40684068msgctxt "add new on admin bar"
    40694069msgid "Presto Media"
    40704070msgstr ""
    40714071
    4072 #: Services/VideoPostType.php:405
     4072#: Services/VideoPostType.php:407
    40734073msgctxt "Media"
    40744074msgid "Add New"
    40754075msgstr ""
    40764076
    4077 #: Services/VideoPostType.php:406
     4077#: Services/VideoPostType.php:408
    40784078msgid "Add New Media"
    40794079msgstr ""
    40804080
    4081 #: Services/VideoPostType.php:407
     4081#: Services/VideoPostType.php:409
    40824082msgid "New Media"
    40834083msgstr ""
    40844084
    4085 #: Services/VideoPostType.php:409
     4085#: Services/VideoPostType.php:411
    40864086msgid "View Media"
    40874087msgstr ""
    40884088
    4089 #: Services/VideoPostType.php:411
     4089#: Services/VideoPostType.php:413
    40904090msgid "Search Media"
    40914091msgstr ""
    40924092
    4093 #: Services/VideoPostType.php:412
     4093#: Services/VideoPostType.php:414
    40944094msgid "No Media found."
    40954095msgstr ""
    40964096
    4097 #: Services/VideoPostType.php:413
     4097#: Services/VideoPostType.php:415
    40984098msgid "No Media found in Trash."
    40994099msgstr ""
    41004100
    4101 #: Services/VideoPostType.php:414
     4101#: Services/VideoPostType.php:416
    41024102msgid "Filter Media list"
    41034103msgstr ""
    41044104
    4105 #: Services/VideoPostType.php:415
     4105#: Services/VideoPostType.php:417
    41064106msgid "Media list navigation"
    41074107msgstr ""
    41084108
    4109 #: Services/VideoPostType.php:416
     4109#: Services/VideoPostType.php:418
    41104110msgid "Media list"
    41114111msgstr ""
    41124112
    4113 #: Services/VideoPostType.php:417
     4113#: Services/VideoPostType.php:419
    41144114msgid "Media published."
    41154115msgstr ""
    41164116
    4117 #: Services/VideoPostType.php:418
     4117#: Services/VideoPostType.php:420
    41184118msgid "Media published privately."
    41194119msgstr ""
    41204120
    4121 #: Services/VideoPostType.php:419
     4121#: Services/VideoPostType.php:421
    41224122msgid "Media reverted to draft."
    41234123msgstr ""
    41244124
    4125 #: Services/VideoPostType.php:420
     4125#: Services/VideoPostType.php:422
    41264126msgid "Media scheduled."
    41274127msgstr ""
    41284128
    4129 #: Services/VideoPostType.php:421
     4129#: Services/VideoPostType.php:423
    41304130msgid "Media updated."
    41314131msgstr ""
    41324132
    4133 #: Services/VideoPostType.php:468
     4133#: Services/VideoPostType.php:471
    41344134#, php-format
    41354135msgid "Show all %s"
  • presto-player/trunk/presto-player.php

    r3468484 r3484635  
    44 * Plugin URI: http://prestoplayer.com
    55 * Description: A beautiful, fast media player for WordPress.
    6  * Version: 4.1.0
     6 * Version: 4.1.1
    77 * Author: Presto Made, Inc
    88 * Author URI: https://prestoplayer.com/
  • presto-player/trunk/readme.txt

    r3468484 r3484635  
    55Requires at least: 6.3
    66Tested up to: 6.9
    7 Stable tag: 4.1.0
     7Stable tag: 4.1.1
    88Requires PHP: 7.3
    99License: GPLv2 or later
     
    157157== Changelog ==
    158158
     159= 4.1.1 =
     160* Security: Multiple security hardening improvements.
     161
    159162= 4.1.0 =
    160163* New: Automatic caption generation with BunnyCDN.
  • presto-player/trunk/vendor/composer/installed.php

    r3468484 r3484635  
    22    'root' => array(
    33        'name' => 'course/player',
    4         'pretty_version' => 'v4.1.0',
    5         'version' => '4.1.0.0',
    6         'reference' => '2bd337ed2bb551577b4496ecd06fed90574dc6d4',
     4        'pretty_version' => 'v4.1.1',
     5        'version' => '4.1.1.0',
     6        'reference' => '788f19da99c0e35e11897dd50e9a1aae24c68e63',
    77        'type' => 'project',
    88        'install_path' => __DIR__ . '/../../',
     
    4848        ),
    4949        'course/player' => array(
    50             'pretty_version' => 'v4.1.0',
    51             'version' => '4.1.0.0',
    52             'reference' => '2bd337ed2bb551577b4496ecd06fed90574dc6d4',
     50            'pretty_version' => 'v4.1.1',
     51            'version' => '4.1.1.0',
     52            'reference' => '788f19da99c0e35e11897dd50e9a1aae24c68e63',
    5353            'type' => 'project',
    5454            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.