$data spec_json, spec_hash, mode, status, candidate_total, created_by, expires_at. * @return int Id. */ public function insert( array $data ) { global $wpdb; $data['created_at'] = current_time( 'mysql', true ); $wpdb->insert( $this->table(), $data ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return (int) $wpdb->insert_id; // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. } /** * Loads one. * * @param int $id Id. * @return array|null */ public function get( $id ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table(), $id ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return $row ? $this->hydrate( $row ) : null; } /** * Updates columns. * * @param int $id Id. * @param array $fields Fields (arrays JSON-encoded). * @return void */ public function update( $id, array $fields ) { global $wpdb; foreach ( $fields as $key => $value ) { if ( is_array( $value ) ) { $fields[ $key ] = Json::encode( $value ); } } $wpdb->update( $this->table(), $fields, array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. } /** * Lists exports newest first. * * @param int $page Page. * @param int $per_page Per page. * @param int|null $user_id Restrict to creator. * @return array{exports: array>, total: int} */ public function list( $page = 1, $per_page = 20, $user_id = null ) { global $wpdb; $where = $user_id ? $wpdb->prepare( 'WHERE created_by = %d', $user_id ) : ''; // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table()} {$where}" ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i {$where} ORDER BY id DESC LIMIT %d OFFSET %d", $this->table(), (int) $per_page, max( 0, ( (int) $page - 1 ) * (int) $per_page ) ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return array( 'exports' => array_map( array( $this, 'hydrate' ), $rows ? $rows : array() ), 'total' => $total, ); } /** * Exports that are waiting or running. * * @return array> */ public function unfinished() { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE status IN (%s, %s) ORDER BY id ASC LIMIT 50', $this->table(), self::STATUS_PENDING, self::STATUS_RUNNING ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. return array_map( array( $this, 'hydrate' ), $rows ? $rows : array() ); } /** * Expired or over-cap exports to purge. * * @param int $cap_per_user Rows kept per user. * @return array> */ public function purgeable( $cap_per_user = 20 ) { global $wpdb; $now = current_time( 'mysql', true ); $expired = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE expires_at IS NOT NULL AND expires_at < %s LIMIT 200', $this->table(), $now ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $over = $wpdb->get_results( $wpdb->prepare( 'SELECT e.* FROM %i e WHERE (SELECT COUNT(*) FROM %i n WHERE n.created_by = e.created_by AND n.id > e.id) >= %d LIMIT 200', $this->table(), $this->table(), $cap_per_user ), ARRAY_A ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. $rows = array(); foreach ( array_merge( $expired ? $expired : array(), $over ? $over : array() ) as $row ) { $rows[ (int) $row['id'] ] = $this->hydrate( $row ); } return array_values( $rows ); } /** * The creator to restrict export lists to for the current user, or null for all exports. * * Exports are files of customer data, so each user sees only the exports they made. Users who * can manage options (store administrators) see every export. Batches are different: they are * shared store-wide by design. * * @return int|null */ public static function viewer_scope() { return current_user_can( 'manage_options' ) ? null : max( 1, (int) get_current_user_id() ); } /** * Whether the current user can see, download or delete an export. * * @param array $export Export. * @return bool */ public static function viewable( array $export ) { $scope = self::viewer_scope(); return null === $scope || (int) $export['created_by'] === $scope; } /** * Deletes a row. * * @param int $id Id. * @return void */ public function delete( $id ) { global $wpdb; $wpdb->delete( $this->table(), array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB, PluginCheck.Security.DirectDB -- Recast-owned table, prepared query. } /** * Decodes JSON columns and casts ints. * * @param array $row Row. * @return array */ private function hydrate( array $row ) { foreach ( array( 'id', 'rows_written', 'created_by' ) as $int ) { $row[ $int ] = (int) $row[ $int ]; } $row['candidate_total'] = null === $row['candidate_total'] ? null : (int) $row['candidate_total']; $row['file_size'] = null === $row['file_size'] ? null : (int) $row['file_size']; $row['spec'] = Json::decode( $row['spec_json'], array() ); $row['cursor'] = Json::decode( isset( $row['resume_cursor'] ) ? $row['resume_cursor'] : null, array() ); return $row; } }