= self::MAX_TRIES; } /** Saves the video soon, in WP-Cron, once. */ public static function schedule( $post_id, $video_id ) { $args = array( (int) $post_id, (string) $video_id ); if ( ! wp_next_scheduled( self::EVENT, $args ) ) { wp_schedule_single_event( time(), self::EVENT, $args ); } } /** "Try again": the tries start over, now, in place of a retry scheduled for later. */ public static function retry( $post_id, $video_id ) { Videos::update( $post_id, $video_id, array( 'save_tries' => 0, 'save_error' => null ) ); wp_clear_scheduled_hook( self::EVENT, array( (int) $post_id, (string) $video_id ) ); self::schedule( $post_id, $video_id ); spawn_cron(); } /** WP-Cron: download the MP4 and the poster, and add them to the Media Library. */ public static function save( $post_id, $video_id ) { $kept = Videos::find( $post_id, $video_id ); if ( ! $kept || ! empty( $kept['attachment_id'] ) || self::gave_up( $kept ) ) { return; } // One at a time: a webhook and the background check can both ask. $lock = 'ojelio_saving_' . md5( $video_id ); if ( get_transient( $lock ) ) { return; } set_transient( $lock, 1, 15 * MINUTE_IN_SECONDS ); try { $result = self::download_and_add( (int) $post_id, $kept ); } finally { delete_transient( $lock ); } if ( is_wp_error( $result ) ) { $tries = ( isset( $kept['save_tries'] ) ? (int) $kept['save_tries'] : 0 ) + 1; Videos::update( $post_id, $video_id, array( 'save_tries' => $tries, 'save_error' => array( 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'at' => time(), ), ) ); // Space and quota do not free themselves within minutes: no automatic retry. if ( $tries < self::MAX_TRIES && ! in_array( $result->get_error_code(), array( 'ojelio_space', 'ojelio_quota', 'ojelio_type' ), true ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS * $tries, self::EVENT, array( (int) $post_id, (string) $video_id ) ); } elseif ( $tries < self::MAX_TRIES ) { Videos::update( $post_id, $video_id, array( 'save_tries' => self::MAX_TRIES ) ); } return; } Videos::update( $post_id, $video_id, array( 'attachment_id' => $result['video'], 'poster_id' => $result['poster'], 'saved_at' => time(), 'save_error' => null, ) ); } /** * @return array|WP_Error The attachments: array( 'video' => ID, 'poster' => ID or 0 ). */ private static function download_and_add( $post_id, array $kept ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; // Fresh links: the ones a webhook carried may be old by now. $video = Connection::call( 'GET', '/videos/' . rawurlencode( $kept['id'] ) ); if ( is_wp_error( $video ) ) { return $video; } if ( empty( $video['files']['video'] ) ) { return new WP_Error( 'ojelio_no_file', __( 'Ojelio has no file for this video yet.', 'ojelio' ) ); } $bytes = isset( $video['files']['video_bytes'] ) ? (int) $video['files']['video_bytes'] : 0; $room = self::room_for( $bytes ); if ( is_wp_error( $room ) ) { return $room; } self::more_time(); $tmp = download_url( $video['files']['video'], 300 ); if ( is_wp_error( $tmp ) ) { return new WP_Error( 'ojelio_download', /* translators: %s: the reason from WordPress, such as "Operation timed out". */ sprintf( __( 'The MP4 could not be downloaded from Ojelio (%s). The server may have stopped it for taking too long, or blocked it.', 'ojelio' ), $tmp->get_error_message() ) ); } $name = sanitize_file_name( ( '' !== $kept['title'] ? $kept['title'] : 'ojelio-video' ) . '-' . $kept['id'] . '.mp4' ); $video_id = media_handle_sideload( array( 'name' => $name, 'tmp_name' => $tmp ), $post_id, $kept['title'] ); if ( is_wp_error( $video_id ) ) { wp_delete_file( $tmp ); $type = 'upload_error' === $video_id->get_error_code() && false !== stripos( $video_id->get_error_message(), 'type' ); return new WP_Error( $type ? 'ojelio_type' : 'ojelio_save', $type ? __( 'This site does not accept MP4 files in its Media Library (a security plugin or the host may block them).', 'ojelio' ) /* translators: %s: the reason from WordPress. */ : sprintf( __( 'The MP4 could not be added to the Media Library (%s).', 'ojelio' ), $video_id->get_error_message() ) ); } update_post_meta( $video_id, '_ojelio_video', $kept['id'] ); // The poster is a nicety: a video saved without it is still saved. $poster_id = 0; if ( ! empty( $video['files']['poster'] ) ) { $tmp = download_url( $video['files']['poster'], 60 ); if ( ! is_wp_error( $tmp ) ) { $poster = media_handle_sideload( array( 'name' => sanitize_file_name( 'ojelio-poster-' . $kept['id'] . '.jpg' ), 'tmp_name' => $tmp ), $post_id, /* translators: %s: the video's title. */ sprintf( __( 'Poster of %s', 'ojelio' ), $kept['title'] ) ); if ( is_wp_error( $poster ) ) { wp_delete_file( $tmp ); } else { $poster_id = (int) $poster; set_post_thumbnail( $video_id, $poster_id ); } } } return array( 'video' => (int) $video_id, 'poster' => $poster_id ); } /** Whether the site has room for a file of this size: multisite's quota, then the disk. */ public static function room_for( $bytes ) { if ( is_multisite() && ! get_site_option( 'upload_space_check_disabled' ) ) { $available = (int) get_upload_space_available(); if ( $bytes && $available < $bytes ) { return new WP_Error( 'ojelio_quota', /* translators: 1: the video's size, 2: the space left, such as "12 MB". */ sprintf( __( 'This site\'s storage quota is full: the video needs %1$s and %2$s is left. A network administrator can raise the quota.', 'ojelio' ), size_format( $bytes ), size_format( $available ) ) ); } } $free = self::free_disk(); if ( null !== $free && $bytes && $free < $bytes + self::MARGIN ) { return new WP_Error( 'ojelio_space', /* translators: 1: the video's size, 2: the space left. */ sprintf( __( 'The server is almost out of disk space: the video needs %1$s and %2$s is free.', 'ojelio' ), size_format( $bytes ), size_format( $free ) ) ); } return true; } /** * Free bytes where uploads go, or null when the host does not say. The * "ojelio_free_disk" filter can correct it (a host that misreports it, the tests). */ public static function free_disk() { $free = null; if ( function_exists( 'disk_free_space' ) ) { $uploads = wp_upload_dir( null, false ); $found = @disk_free_space( $uploads['basedir'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- some hosts disable it. $free = false === $found ? null : (float) $found; } return apply_filters( 'ojelio_free_disk', $free ); } /** PHP's time limit for this request, in seconds (0: none). */ public static function time_limit() { return (int) ini_get( 'max_execution_time' ); } private static function more_time() { if ( function_exists( 'set_time_limit' ) && false === strpos( (string) ini_get( 'disable_functions' ), 'set_time_limit' ) ) { // A long MP4 can take more than the default 30 seconds to download in the // background task; hosts that forbid it keep their limit (time_limit() warns). @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged } } }