From bf9b5f33572480c54adc29d139b52b324a919af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 16 Jul 2021 12:20:01 +0200 Subject: [PATCH 01/29] Logic for deciding between direct download and proxy download --- src/php/class-api-facade.php | 1 + src/php/frontend/page.php | 43 ++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/php/class-api-facade.php b/src/php/class-api-facade.php index e40aafb77..49c8f7890 100644 --- a/src/php/class-api-facade.php +++ b/src/php/class-api-facade.php @@ -226,6 +226,7 @@ private static function list_files( $parent_id, $fields, $order_by, $pagination_ 'name', 'mimeType', 'createdTime', + 'copyRequiresWriterPermission', 'imageMediaMetadata' => array( 'width', 'height', 'time' ), 'videoMediaMetadata' => array( 'width', 'height' ), 'webContentLink', diff --git a/src/php/frontend/page.php b/src/php/frontend/page.php index abbea9012..365284a31 100644 --- a/src/php/frontend/page.php +++ b/src/php/frontend/page.php @@ -402,6 +402,8 @@ function videos( $parent_id, $pagination_helper, $options ) { 'webContentLink', 'thumbnailLink', 'videoMediaMetadata' => array( 'width', 'height' ), + 'copyRequiresWriterPermission', + 'permissions', // TODO: Narrow down. ) ), $options->get( 'image_ordering' ), @@ -416,7 +418,7 @@ static function( $video ) use ( &$options ) { 'mimeType' => $video['mimeType'], 'width' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['width'] : '0', 'height' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => resolve_video_url( $video['webContentLink'] ), + 'src' => resolve_video_url( $video['webContentLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), ); }, $videos @@ -431,10 +433,36 @@ static function( $video ) use ( &$options ) { * Finds the correct URL so that a video would load in the browser. * * @param string $web_content_url The webContentLink returned by Google Drive API. + * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. + * @param array $permissions The file permissions. + * + * @return string The resolved video URL. + * + * @SuppressWarnings(PHPMD.LongVariable) + */ +function resolve_video_url( $web_content_url, $copy_requires_writer_permission, $permissions ) { + if ( $copy_requires_writer_permission ) { + return get_proxy_video_url(); + } + foreach ( $permissions as $permission ) { + if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { // TODO: type: domain?, domain: ??? + return get_direct_video_url( $web_content_url ); + } + } + // TODO: For files not owned by me, I can't view sharing permissions. Also, check what happens in shared drives... + return get_proxy_video_url(); +} + +/** + * Returns the direct URL for a video. + * + * Goes through the download warning and returns the direct download URL for a video. + * + * @param string $web_content_url The webContentLink returned by Google Drive API. * * @return string The resolved video URL. */ -function resolve_video_url( $web_content_url ) { +function get_direct_video_url( $web_content_url ) { $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); $url = $web_content_url; $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); @@ -458,3 +486,14 @@ function resolve_video_url( $web_content_url ) { } return $url; } + +/** + * Returns the proxy URL for a video. + * + * Sets up a proxy in WordPress and returns the address of this proxy. + * + * @return string The resolved video URL. + */ +function get_proxy_video_url() { + return 'TODO: PROXY LINK'; +} From 7999c366261176c982f23df5ea1f088b0e5ef3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 30 Jul 2021 21:55:55 +0200 Subject: [PATCH 02/29] Direct links to videos not owned by me --- src/php/frontend/page.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/php/frontend/page.php b/src/php/frontend/page.php index 365284a31..835f7f5b7 100644 --- a/src/php/frontend/page.php +++ b/src/php/frontend/page.php @@ -400,6 +400,7 @@ function videos( $parent_id, $pagination_helper, $options ) { 'id', 'mimeType', 'webContentLink', + 'webViewLink', 'thumbnailLink', 'videoMediaMetadata' => array( 'width', 'height' ), 'copyRequiresWriterPermission', @@ -418,7 +419,7 @@ static function( $video ) use ( &$options ) { 'mimeType' => $video['mimeType'], 'width' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['width'] : '0', 'height' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => resolve_video_url( $video['webContentLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), + 'src' => resolve_video_url( $video['id'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), ); }, $videos @@ -433,6 +434,7 @@ static function( $video ) use ( &$options ) { * Finds the correct URL so that a video would load in the browser. * * @param string $web_content_url The webContentLink returned by Google Drive API. + * @param string $web_view_url The webViewLink returned by Google Drive API. * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. * @param array $permissions The file permissions. * @@ -440,16 +442,21 @@ static function( $video ) use ( &$options ) { * * @SuppressWarnings(PHPMD.LongVariable) */ -function resolve_video_url( $web_content_url, $copy_requires_writer_permission, $permissions ) { +function resolve_video_url( $id, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { if ( $copy_requires_writer_permission ) { return get_proxy_video_url(); } foreach ( $permissions as $permission ) { - if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { // TODO: type: domain?, domain: ??? + if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { return get_direct_video_url( $web_content_url ); } } - // TODO: For files not owned by me, I can't view sharing permissions. Also, check what happens in shared drives... + $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); + $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? + if ( 200 === $response->getStatusCode() ) { + return get_direct_video_url( $web_content_url ); + } + // TODO: Check what happens in shared drives... return get_proxy_video_url(); } @@ -465,7 +472,7 @@ function resolve_video_url( $web_content_url, $copy_requires_writer_permission, function get_direct_video_url( $web_content_url ) { $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); $url = $web_content_url; - $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); + $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? if ( $response->hasHeader( 'Set-Cookie' ) && 0 === mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { // Handle virus scan warning. From ba2046d0ddd5140a83d7e2b2384610d48cc4524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sat, 11 Sep 2021 12:21:59 +0200 Subject: [PATCH 03/29] Fixed web_view_url not being used --- src/php/frontend/page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/page.php b/src/php/frontend/page.php index ad8e4e041..71b7f1e16 100644 --- a/src/php/frontend/page.php +++ b/src/php/frontend/page.php @@ -452,7 +452,7 @@ function resolve_video_url( $id, $web_content_url, $web_view_url, $copy_requires } } $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? + $response = $http_client->get( $web_view_url, array( 'allow_redirects' => false ) ); // TODO: Use promises? if ( 200 === $response->getStatusCode() ) { return get_direct_video_url( $web_content_url ); } From 5fc381fe27597ba3496efff3811c3e96442d33e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sat, 11 Sep 2021 13:59:07 +0200 Subject: [PATCH 04/29] Checked what happens in shared drives --- src/php/frontend/page.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/php/frontend/page.php b/src/php/frontend/page.php index 71b7f1e16..2ede25e7d 100644 --- a/src/php/frontend/page.php +++ b/src/php/frontend/page.php @@ -456,7 +456,6 @@ function resolve_video_url( $id, $web_content_url, $web_view_url, $copy_requires if ( 200 === $response->getStatusCode() ) { return get_direct_video_url( $web_content_url ); } - // TODO: Check what happens in shared drives... return get_proxy_video_url(); } From 58c8747f84db53251874ae20342344265a8a152f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 30 Jan 2022 20:32:13 +0100 Subject: [PATCH 05/29] Redirecting private videos to proxy endpoint --- src/php/class-api-facade.php | 3 ++ src/php/frontend/page.php | 43 +++++++++++++------ src/php/frontend/video-proxy.php | 57 ++++++++++++++++++++++++++ src/php/skaut-google-drive-gallery.php | 2 + 4 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 src/php/frontend/video-proxy.php diff --git a/src/php/class-api-facade.php b/src/php/class-api-facade.php index 49c8f7890..f93d1662b 100644 --- a/src/php/class-api-facade.php +++ b/src/php/class-api-facade.php @@ -225,13 +225,16 @@ private static function list_files( $parent_id, $fields, $order_by, $pagination_ 'id', 'name', 'mimeType', + 'size', 'createdTime', 'copyRequiresWriterPermission', 'imageMediaMetadata' => array( 'width', 'height', 'time' ), 'videoMediaMetadata' => array( 'width', 'height' ), 'webContentLink', + 'webViewLink', 'thumbnailLink', 'description', + 'permissions' => array( 'type', 'role' ), ) ) ) { throw new \Sgdg\Exceptions\Unsupported_Value_Exception( $fields, 'list_files' ); diff --git a/src/php/frontend/page.php b/src/php/frontend/page.php index 29bdf68b4..c3340a729 100644 --- a/src/php/frontend/page.php +++ b/src/php/frontend/page.php @@ -40,7 +40,7 @@ function handle_ajax() { } /** - * Actually handles the "gallery" AJAX endpoint. + * Actually handles the "page" AJAX endpoint. * * Returns a list of directories and a list of images. * @@ -407,12 +407,13 @@ function videos( $parent_id, $pagination_helper, $options ) { array( 'id', 'mimeType', + 'size', 'webContentLink', 'webViewLink', 'thumbnailLink', 'videoMediaMetadata' => array( 'width', 'height' ), 'copyRequiresWriterPermission', - 'permissions', // TODO: Narrow down. + 'permissions' => array( 'type', 'role' ), ) ), $options->get( 'image_ordering' ), @@ -427,7 +428,7 @@ static function( $video ) use ( &$options ) { 'mimeType' => $video['mimeType'], 'width' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['width'] : '0', 'height' => array_key_exists( 'videoMediaMetadata', $video ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => resolve_video_url( $video['id'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), + 'src' => resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), ); }, $videos @@ -441,18 +442,21 @@ static function( $video ) use ( &$options ) { * * Finds the correct URL so that a video would load in the browser. * - * @param string $web_content_url The webContentLink returned by Google Drive API. - * @param string $web_view_url The webViewLink returned by Google Drive API. - * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. - * @param array $permissions The file permissions. + * @param string $video_id The ID of the video. + * @param string $mime_type The MIME type of the video. + * @param int $size The size of the video in bytes. + * @param string $web_content_url The webContentLink returned by Google Drive API. + * @param string $web_view_url The webViewLink returned by Google Drive API. + * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. + * @param array $permissions The file permissions. * * @return string The resolved video URL. * * @SuppressWarnings(PHPMD.LongVariable) */ -function resolve_video_url( $id, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { +function resolve_video_url( $video_id, $mime_type, $size, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { if ( $copy_requires_writer_permission ) { - return get_proxy_video_url(); + return get_proxy_video_url( $video_id, $mime_type, $size ); } foreach ( $permissions as $permission ) { if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { @@ -464,7 +468,7 @@ function resolve_video_url( $id, $web_content_url, $web_view_url, $copy_requires if ( 200 === $response->getStatusCode() ) { return get_direct_video_url( $web_content_url ); } - return get_proxy_video_url(); + return get_proxy_video_url( $video_id, $mime_type, $size ); } /** @@ -506,8 +510,23 @@ function get_direct_video_url( $web_content_url ) { * * Sets up a proxy in WordPress and returns the address of this proxy. * + * @param string $video_id The ID of the video. + * @param string $mime_type The MIME type of the video. + * @param int $size The size of the video in bytes. + * * @return string The resolved video URL. */ -function get_proxy_video_url() { - return 'TODO: PROXY LINK'; +function get_proxy_video_url( $video_id, $mime_type, $size ) { + $gallery_hash = \Sgdg\safe_get_string_variable( 'hash' ); + $video_hash = hash( 'sha256', $gallery_hash . $video_id ); + set_transient( + 'sgdg_video_proxy_' . $video_hash, + array( + 'id' => $video_id, + 'mimeType' => $mime_type, + 'size' => $size, + ), + DAY_IN_SECONDS + ); + return admin_url( 'admin-ajax.php?action=video_proxy&gallery_hash=' . $gallery_hash . '&video_hash=' . $video_hash ); } diff --git a/src/php/frontend/video-proxy.php b/src/php/frontend/video-proxy.php new file mode 100644 index 000000000..3f7151043 --- /dev/null +++ b/src/php/frontend/video-proxy.php @@ -0,0 +1,57 @@ +getMessage() ) ); + } + wp_die(); + } +} + +/** + * Actually handles the "video_proxy" AJAX endpoint. + * + * Streams the video from Google Drive through the website server. + * + * @return void + */ +function ajax_handler_body() { + // < content-length: 5091642 + // < content-type: video/mp4 + // if range request: + // Accept-ranges + // < content-range: bytes 0-5091641/5091642 + header( 'Content-Disposition: attachment' ); + if ( isset( $_SERVER['HTTP_RANGE'] ) ) { + var_dump( $_SERVER['HTTP_RANGE'] ); + } +} diff --git a/src/php/skaut-google-drive-gallery.php b/src/php/skaut-google-drive-gallery.php index 31f8f74f8..1bf98cf65 100644 --- a/src/php/skaut-google-drive-gallery.php +++ b/src/php/skaut-google-drive-gallery.php @@ -79,6 +79,7 @@ require_once __DIR__ . '/frontend/shortcode.php'; require_once __DIR__ . '/frontend/page.php'; require_once __DIR__ . '/frontend/gallery.php'; +require_once __DIR__ . '/frontend/video-proxy.php'; require_once __DIR__ . '/admin/google-api-lib.php'; require_once __DIR__ . '/admin/admin-pages.php'; @@ -97,6 +98,7 @@ function init() { \Sgdg\Frontend\Block\register(); \Sgdg\Frontend\Page\register(); \Sgdg\Frontend\Gallery\register(); + \Sgdg\Frontend\Video_Proxy\register(); \Sgdg\Admin\AdminPages\register(); \Sgdg\Admin\TinyMCE\register(); } From 938e4c4a45a24f66b1143f2bf023c67b7d82cb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 14:10:12 +0200 Subject: [PATCH 06/29] Typo fix --- src/php/frontend/class-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/class-page.php b/src/php/frontend/class-page.php index 511c0a94f..1edcae540 100644 --- a/src/php/frontend/class-page.php +++ b/src/php/frontend/class-page.php @@ -325,7 +325,7 @@ static function( &$item ) { } /** - * Returns a list of images in a directory + * Returns a list of videos in a directory * * @param string $parent_id A directory to list items of. * @param \Sgdg\Frontend\Pagination_Helper $pagination_helper An initialized pagination helper. From e3b4397e3848fc61f3b8d96b28ec1c7e9a0f9061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 14:29:36 +0200 Subject: [PATCH 07/29] Converted Video_Proxy to a class and using the new ajax helper --- src/php/class-main.php | 2 +- src/php/frontend/class-video-proxy.php | 55 +++++++++++++++++++++++++ src/php/frontend/video-proxy.php | 57 -------------------------- src/php/skaut-google-drive-gallery.php | 3 +- 4 files changed, 57 insertions(+), 60 deletions(-) create mode 100644 src/php/frontend/class-video-proxy.php delete mode 100644 src/php/frontend/video-proxy.php diff --git a/src/php/class-main.php b/src/php/class-main.php index a788e086e..f076c55bc 100644 --- a/src/php/class-main.php +++ b/src/php/class-main.php @@ -24,7 +24,7 @@ public function __construct() { new \Sgdg\Frontend\Block(); new \Sgdg\Frontend\Page(); new \Sgdg\Frontend\Gallery(); - \Sgdg\Frontend\Video_Proxy\register(); + new \Sgdg\Frontend\Video_Proxy(); new \Sgdg\Admin\Settings_Pages(); new \Sgdg\Admin\TinyMCE_Plugin(); } diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php new file mode 100644 index 000000000..758dc34cb --- /dev/null +++ b/src/php/frontend/class-video-proxy.php @@ -0,0 +1,55 @@ +getMessage() ) ); - } - wp_die(); - } -} - -/** - * Actually handles the "video_proxy" AJAX endpoint. - * - * Streams the video from Google Drive through the website server. - * - * @return void - */ -function ajax_handler_body() { - // < content-length: 5091642 - // < content-type: video/mp4 - // if range request: - // Accept-ranges - // < content-range: bytes 0-5091641/5091642 - header( 'Content-Disposition: attachment' ); - if ( isset( $_SERVER['HTTP_RANGE'] ) ) { - var_dump( $_SERVER['HTTP_RANGE'] ); - } -} diff --git a/src/php/skaut-google-drive-gallery.php b/src/php/skaut-google-drive-gallery.php index 6a9d92fa0..a7ce04dd1 100644 --- a/src/php/skaut-google-drive-gallery.php +++ b/src/php/skaut-google-drive-gallery.php @@ -84,8 +84,7 @@ require_once __DIR__ . '/frontend/class-page.php'; require_once __DIR__ . '/frontend/class-shortcode.php'; require_once __DIR__ . '/frontend/class-single-page-pagination-helper.php'; - -require_once __DIR__ . '/frontend/video-proxy.php'; +require_once __DIR__ . '/frontend/class-video-proxy.php'; require_once __DIR__ . '/admin/class-oauth-helpers.php'; require_once __DIR__ . '/admin/class-settings-pages.php'; From 541501e4d9c6d093c19345207943e478806c4e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 14:30:44 +0200 Subject: [PATCH 08/29] Added constructor side-effect annotation for phan --- src/php/frontend/class-video-proxy.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 758dc34cb..3b5331691 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -11,6 +11,8 @@ * Contains all the functions used to handle the "video_proxy" AJAX endpoint. * * The "video_proxy" AJAX enpoint gets called for private videos over a certain size and serves the video through the webiste server. + * + * @phan-constructor-used-for-side-effects */ class Video_Proxy { From 4ee47925b6a17d481015bba45e1a78ed5ec8bca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 14:41:13 +0200 Subject: [PATCH 09/29] If video is bigger than 25MB, then using proxy mode automatically --- src/php/frontend/class-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/class-page.php b/src/php/frontend/class-page.php index 1edcae540..522c2779e 100644 --- a/src/php/frontend/class-page.php +++ b/src/php/frontend/class-page.php @@ -388,7 +388,7 @@ static function( $video ) use ( &$options ) { * @SuppressWarnings(PHPMD.LongVariable) */ private static function resolve_video_url( $video_id, $mime_type, $size, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { - if ( $copy_requires_writer_permission ) { + if ( $copy_requires_writer_permission || $size > 25165824 ) { return self::get_proxy_video_url( $video_id, $mime_type, $size ); } foreach ( $permissions as $permission ) { From 9d3548fb580b4e02202b0fb6056db87e5c84670a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 19:21:55 +0200 Subject: [PATCH 10/29] Fixed private function being called outside of class --- src/php/frontend/class-video-proxy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 3b5331691..c050d328e 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -42,7 +42,7 @@ public static function handle_ajax() { * * @return void */ - private static function ajax_handler_body() { + public static function ajax_handler_body() { // < content-length: 5091642 // < content-type: video/mp4 // if range request: From 2f790245d29aba77273db76c5953ba8fa472079e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 19:45:08 +0200 Subject: [PATCH 11/29] More robust API response parsing --- src/php/frontend/class-api-fields.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/php/frontend/class-api-fields.php b/src/php/frontend/class-api-fields.php index 30c107d54..fdb6355c4 100644 --- a/src/php/frontend/class-api-fields.php +++ b/src/php/frontend/class-api-fields.php @@ -103,15 +103,19 @@ public function format() { public function parse_response( $response ) { $ret = array(); foreach ( $this->fields as $key => $value ) { - if ( is_string( $key ) && is_array( $value ) ) { + if ( is_array( $value ) ) { foreach ( $value as $subvalue ) { - $ret[ $key ][ $subvalue ] = $response->$key->$subvalue; + if ( property_exists( $response, strval( $key ) ) && property_exists( $response->$key, $subvalue ) ) { + $ret[ $key ][ $subvalue ] = $response->$key->$subvalue; + } } } else { if ( 'id' === $value ) { $ret['id'] = $response->getMimeType() === 'application/vnd.google-apps.shortcut' ? $response->getShortcutDetails()->getTargetId() : $response->getId(); } else { - $ret[ strval( $value ) ] = $response->$value; + if ( property_exists( $response, $value ) ) { + $ret[ strval( $value ) ] = $response->$value; + } } } } From 16855fa632216f57c458fea8729f6ef520d4c64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 18 Jul 2022 19:48:46 +0200 Subject: [PATCH 12/29] Fixed missing permissions value --- src/php/frontend/class-page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/class-page.php b/src/php/frontend/class-page.php index 522c2779e..e3cc96fa3 100644 --- a/src/php/frontend/class-page.php +++ b/src/php/frontend/class-page.php @@ -361,7 +361,7 @@ static function( $video ) use ( &$options ) { 'mimeType' => $video['mimeType'], 'width' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'width', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['width'] : '0', 'height' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'height', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], $video['permissions'] ), + 'src' => self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], array_key_exists( 'permissions', $video ) ? $video['permissions'] : array() ), ); }, $videos From 5d91446e80f215bb78ac0c304607879f7fa5cb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:10:30 +0200 Subject: [PATCH 13/29] Added video proxying --- src/php/frontend/class-page.php | 2 +- src/php/frontend/class-video-proxy.php | 102 +++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/php/frontend/class-page.php b/src/php/frontend/class-page.php index e3cc96fa3..3c75d8e30 100644 --- a/src/php/frontend/class-page.php +++ b/src/php/frontend/class-page.php @@ -461,6 +461,6 @@ private static function get_proxy_video_url( $video_id, $mime_type, $size ) { ), DAY_IN_SECONDS ); - return admin_url( 'admin-ajax.php?action=video_proxy&gallery_hash=' . $gallery_hash . '&video_hash=' . $video_hash ); + return admin_url( 'admin-ajax.php?action=video_proxy&video_hash=' . $video_hash ); } } diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index c050d328e..f2664e56a 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -43,15 +43,103 @@ public static function handle_ajax() { * @return void */ public static function ajax_handler_body() { - // < content-length: 5091642 - // < content-type: video/mp4 - // if range request: - // Accept-ranges - // < content-range: bytes 0-5091641/5091642 + $video_hash = \Sgdg\GET_Helpers::get_string_variable( 'video_hash' ); + $transient = get_transient( 'sgdg_video_proxy_' . $video_hash ); + if ( false === $transient ) { + http_response_code( 404 ); + die(); + } + $video_id = $transient['id']; + $mime_type = $transient['mimeType']; + $size = $transient['size']; + + header( 'Accept-Ranges: bytes' ); header( 'Content-Disposition: attachment' ); - if ( isset( $_SERVER['HTTP_RANGE'] ) ) { - var_dump( $_SERVER['HTTP_RANGE'] ); + header( 'Content-Length: ' . $size ); + header( 'Content-Type: ' . $mime_type ); + // The headers above should be set even when the range request fails. + list( $start, $end ) = self::resolve_range( $size ); + http_response_code( 206 ); + header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size ); + $raw_client = \Sgdg\API_Client::get_raw_client(); + // TODO: Move this into API_Client + $access_token = get_option( 'sgdg_access_token', false ); + if ( false === $access_token ) { + throw new \Sgdg\Exceptions\Plugin_Not_Authorized_Exception(); + } + $raw_client->setAccessToken( $access_token ); + + if ( $raw_client->isAccessTokenExpired() ) { + $raw_client->fetchAccessTokenWithRefreshToken( $raw_client->getRefreshToken() ); + $new_access_token = $raw_client->getAccessToken(); + $merged_access_token = array_merge( $access_token, $new_access_token ); + update_option( 'sgdg_access_token', $merged_access_token ); + } + $http = $raw_client->authorize(); + $response = $http->request( + 'GET', + 'drive/v3/files/' . $video_id, + array( + 'query' => array( + 'alt' => 'media' + ), + 'headers' => array( + 'Range' => 'bytes=' . $start . '-' . $end, + ), + 'stream' => true, + ) + ); + $stream = $response->getBody()->detach(); + if ( is_null( $stream ) ) { + http_response_code( 500 ); + die(); } + ob_end_clean(); + fpassthru( $stream ); } + /** + * Resolves the start and end of a HTTP range request. + * + * @param int $size The size of the video file in bytes. + * + * @return array{0: int, 1: int}|never The start and end of the range. + */ + private static function resolve_range( $size ) { + if ( ! isset( $_SERVER['HTTP_RANGE'] ) ) { + return array( 0, $size - 1 ); + } + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $header = sanitize_text_field( wp_unslash( strval( $_SERVER['HTTP_RANGE'] ) ) ); + if ( ! str_starts_with( $header, 'bytes=' ) ) { + http_response_code( 416 ); + die(); + } + $header = substr( $header, 6 ); + // Multipart range requests are not supported. + if ( str_contains( $header, ',' ) ) { + http_response_code( 416 ); + die(); + } + $limits = explode( '-', $header ); + if ( 2 !== count( $limits ) ) { + http_response_code( 416 ); + die(); + } + $raw_start = $limits[0]; + $raw_end = $limits[1]; + $start = is_numeric( $raw_start ) ? intval( $raw_start ) : 0; + $end = is_numeric( $raw_end ) ? intval( $raw_end ) : $size - 1; + if ( $start < 0 ) { + $start = 0; + } + if ( $end >= $size ) { + $end = $size - 1; + } + if ( $start > $end ) { + http_response_code( 416 ); + die(); + } + return array( $start, $end ); + } } From 31e238b63f528710fed99583a33bc08a3aba43b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:12:01 +0200 Subject: [PATCH 14/29] Added TODO --- src/php/frontend/class-video-proxy.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index f2664e56a..401c34328 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -76,6 +76,7 @@ public static function ajax_handler_body() { update_option( 'sgdg_access_token', $merged_access_token ); } $http = $raw_client->authorize(); + // X-Goog-Drive-Resource-Keys header $response = $http->request( 'GET', 'drive/v3/files/' . $video_id, From 33cd9eec579568db450daec9521f62bdc258c70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:19:25 +0200 Subject: [PATCH 15/29] API_Client::get_raw_client() renamed to get_unauthorized_raw_client() --- src/php/admin/class-oauth-helpers.php | 6 +++--- src/php/class-api-client.php | 6 +++--- src/php/frontend/class-video-proxy.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/php/admin/class-oauth-helpers.php b/src/php/admin/class-oauth-helpers.php index eed1d578f..3284b48a4 100644 --- a/src/php/admin/class-oauth-helpers.php +++ b/src/php/admin/class-oauth-helpers.php @@ -23,7 +23,7 @@ public static function grant_redirect() { return; } - $client = \Sgdg\API_Client::get_raw_client(); + $client = \Sgdg\API_Client::get_unauthorized_raw_client(); $auth_url = $client->createAuthUrl(); header( 'Location: ' . esc_url_raw( $auth_url ) ); } @@ -43,7 +43,7 @@ public static function grant_return() { add_settings_error( 'general', 'oauth_failed', esc_html__( 'Google API hasn\'t returned an authentication code. Please try again.', 'skaut-google-drive-gallery' ), 'error' ); } if ( count( get_settings_errors() ) === 0 && false === get_option( 'sgdg_access_token', false ) ) { - $client = \Sgdg\API_Client::get_raw_client(); + $client = \Sgdg\API_Client::get_unauthorized_raw_client(); try { $client->fetchAccessTokenWithAuthCode( \Sgdg\GET_Helpers::get_string_variable( 'code' ) ); $access_token = $client->getAccessToken(); @@ -84,7 +84,7 @@ public static function revoke() { return; } - $client = \Sgdg\API_Client::get_raw_client(); + $client = \Sgdg\API_Client::get_unauthorized_raw_client(); try { $client->revokeToken(); delete_option( 'sgdg_access_token' ); diff --git a/src/php/class-api-client.php b/src/php/class-api-client.php index 47e5bc661..b39d605ae 100644 --- a/src/php/class-api-client.php +++ b/src/php/class-api-client.php @@ -42,11 +42,11 @@ class API_Client { private static $pending_requests; /** - * Returns a fully set-up Google client. + * Returns a Google client with set-up app info, but without authorization. * * @return \Sgdg\Vendor\Google\Client */ - public static function get_raw_client() { + public static function get_unauthorized_raw_client() { $raw_client = self::$raw_client; if ( null === $raw_client ) { $raw_client = new \Sgdg\Vendor\Google\Client(); @@ -75,7 +75,7 @@ public static function get_raw_client() { public static function get_drive_client() { $drive_client = self::$drive_client; if ( null === $drive_client ) { - $raw_client = self::get_raw_client(); + $raw_client = self::get_unauthorized_raw_client(); $access_token = get_option( 'sgdg_access_token', false ); if ( false === $access_token ) { throw new \Sgdg\Exceptions\Plugin_Not_Authorized_Exception(); diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 401c34328..abee26ed7 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -61,7 +61,7 @@ public static function ajax_handler_body() { list( $start, $end ) = self::resolve_range( $size ); http_response_code( 206 ); header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size ); - $raw_client = \Sgdg\API_Client::get_raw_client(); + $raw_client = \Sgdg\API_Client::get_unauthorized_raw_client(); // TODO: Move this into API_Client $access_token = get_option( 'sgdg_access_token', false ); if ( false === $access_token ) { From 2700bd8e5f174f3d09ad297f3f19ea1769abad6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:23:05 +0200 Subject: [PATCH 16/29] Split out API_Client::get_authorized_raw_client() --- src/php/class-api-client.php | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/php/class-api-client.php b/src/php/class-api-client.php index b39d605ae..06d0c89c3 100644 --- a/src/php/class-api-client.php +++ b/src/php/class-api-client.php @@ -65,6 +65,28 @@ public static function get_unauthorized_raw_client() { return $raw_client; } + /** + * Returns a fully configured and authorized Google client. + * + * @return \Sgdg\Vendor\Google\Client + */ + public static function get_authorized_raw_client() { + $raw_client = self::get_unauthorized_raw_client(); + $access_token = get_option( 'sgdg_access_token', false ); + if ( false === $access_token ) { + throw new \Sgdg\Exceptions\Plugin_Not_Authorized_Exception(); + } + $raw_client->setAccessToken( $access_token ); + + if ( $raw_client->isAccessTokenExpired() ) { + $raw_client->fetchAccessTokenWithRefreshToken( $raw_client->getRefreshToken() ); + $new_access_token = $raw_client->getAccessToken(); + $merged_access_token = array_merge( $access_token, $new_access_token ); + update_option( 'sgdg_access_token', $merged_access_token ); + } + return $raw_client; + } + /** * Returns a fully set-up Google Drive API client. * @@ -75,19 +97,7 @@ public static function get_unauthorized_raw_client() { public static function get_drive_client() { $drive_client = self::$drive_client; if ( null === $drive_client ) { - $raw_client = self::get_unauthorized_raw_client(); - $access_token = get_option( 'sgdg_access_token', false ); - if ( false === $access_token ) { - throw new \Sgdg\Exceptions\Plugin_Not_Authorized_Exception(); - } - $raw_client->setAccessToken( $access_token ); - - if ( $raw_client->isAccessTokenExpired() ) { - $raw_client->fetchAccessTokenWithRefreshToken( $raw_client->getRefreshToken() ); - $new_access_token = $raw_client->getAccessToken(); - $merged_access_token = array_merge( $access_token, $new_access_token ); - update_option( 'sgdg_access_token', $merged_access_token ); - } + $raw_client = self::get_authorized_raw_client(); $drive_client = new \Sgdg\Vendor\Google\Service\Drive( $raw_client ); self::$drive_client = $drive_client; } From ba6d8ce997b5c5f92f3d22dc31d36a89596cac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:26:32 +0200 Subject: [PATCH 17/29] Using API_Client::get_authorized_client() in video proxy --- src/php/frontend/class-video-proxy.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index abee26ed7..e244f1032 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -61,22 +61,9 @@ public static function ajax_handler_body() { list( $start, $end ) = self::resolve_range( $size ); http_response_code( 206 ); header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size ); - $raw_client = \Sgdg\API_Client::get_unauthorized_raw_client(); - // TODO: Move this into API_Client - $access_token = get_option( 'sgdg_access_token', false ); - if ( false === $access_token ) { - throw new \Sgdg\Exceptions\Plugin_Not_Authorized_Exception(); - } - $raw_client->setAccessToken( $access_token ); - if ( $raw_client->isAccessTokenExpired() ) { - $raw_client->fetchAccessTokenWithRefreshToken( $raw_client->getRefreshToken() ); - $new_access_token = $raw_client->getAccessToken(); - $merged_access_token = array_merge( $access_token, $new_access_token ); - update_option( 'sgdg_access_token', $merged_access_token ); - } - $http = $raw_client->authorize(); - // X-Goog-Drive-Resource-Keys header + $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); + // TODO: X-Goog-Drive-Resource-Keys header? $response = $http->request( 'GET', 'drive/v3/files/' . $video_id, From b00f518fbc8f81016962b17001dfeda88bffe184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:26:50 +0200 Subject: [PATCH 18/29] Simplified video proxy --- src/php/frontend/class-video-proxy.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index e244f1032..823c79cc9 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -49,24 +49,21 @@ public static function ajax_handler_body() { http_response_code( 404 ); die(); } - $video_id = $transient['id']; - $mime_type = $transient['mimeType']; - $size = $transient['size']; header( 'Accept-Ranges: bytes' ); header( 'Content-Disposition: attachment' ); - header( 'Content-Length: ' . $size ); - header( 'Content-Type: ' . $mime_type ); - // The headers above should be set even when the range request fails. - list( $start, $end ) = self::resolve_range( $size ); + header( 'Content-Length: ' . $transient['size'] ); + header( 'Content-Type: ' . $transient['mimeType'] ); + // The headers above should be set before the call to `resolve_range()` so that they are present even if the range request fails. + list( $start, $end ) = self::resolve_range( $transient['size'] ); http_response_code( 206 ); - header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size ); + header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $transient['size'] ); $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); // TODO: X-Goog-Drive-Resource-Keys header? $response = $http->request( 'GET', - 'drive/v3/files/' . $video_id, + 'drive/v3/files/' . $transient['id'], array( 'query' => array( 'alt' => 'media' From 31d516375882585dc55858e7fbdf40cb5183a31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:27:44 +0200 Subject: [PATCH 19/29] Code style fixes --- src/php/frontend/class-video-proxy.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 823c79cc9..de7b3b94e 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -56,8 +56,8 @@ public static function ajax_handler_body() { header( 'Content-Type: ' . $transient['mimeType'] ); // The headers above should be set before the call to `resolve_range()` so that they are present even if the range request fails. list( $start, $end ) = self::resolve_range( $transient['size'] ); - http_response_code( 206 ); header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $transient['size'] ); + http_response_code( 206 ); $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); // TODO: X-Goog-Drive-Resource-Keys header? @@ -79,6 +79,7 @@ public static function ajax_handler_body() { http_response_code( 500 ); die(); } + ob_end_clean(); fpassthru( $stream ); } From 45e336c1f043e15105a054a0efc08d9ba5703fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:33:19 +0200 Subject: [PATCH 20/29] Lint fixes --- src/php/class-api-client.php | 6 +++--- src/php/frontend/class-video-proxy.php | 18 +++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/php/class-api-client.php b/src/php/class-api-client.php index 06d0c89c3..fdb18e7e3 100644 --- a/src/php/class-api-client.php +++ b/src/php/class-api-client.php @@ -68,6 +68,8 @@ public static function get_unauthorized_raw_client() { /** * Returns a fully configured and authorized Google client. * + * @throws \Sgdg\Exceptions\Plugin_Not_Authorized_Exception Not authorized. + * * @return \Sgdg\Vendor\Google\Client */ public static function get_authorized_raw_client() { @@ -90,14 +92,12 @@ public static function get_authorized_raw_client() { /** * Returns a fully set-up Google Drive API client. * - * @throws \Sgdg\Exceptions\Plugin_Not_Authorized_Exception Not authorized. - * * @return \Sgdg\Vendor\Google\Service\Drive */ public static function get_drive_client() { $drive_client = self::$drive_client; if ( null === $drive_client ) { - $raw_client = self::get_authorized_raw_client(); + $raw_client = self::get_authorized_raw_client(); $drive_client = new \Sgdg\Vendor\Google\Service\Drive( $raw_client ); self::$drive_client = $drive_client; } diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index de7b3b94e..57cc180db 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -41,6 +41,8 @@ public static function handle_ajax() { * Streams the video from Google Drive through the website server. * * @return void + * + * @SuppressWarnings(PHPMD.ExitExpression) */ public static function ajax_handler_body() { $video_hash = \Sgdg\GET_Helpers::get_string_variable( 'video_hash' ); @@ -65,16 +67,16 @@ public static function ajax_handler_body() { 'GET', 'drive/v3/files/' . $transient['id'], array( - 'query' => array( - 'alt' => 'media' + 'query' => array( + 'alt' => 'media', ), 'headers' => array( 'Range' => 'bytes=' . $start . '-' . $end, ), - 'stream' => true, + 'stream' => true, ) ); - $stream = $response->getBody()->detach(); + $stream = $response->getBody()->detach(); if ( is_null( $stream ) ) { http_response_code( 500 ); die(); @@ -90,6 +92,8 @@ public static function ajax_handler_body() { * @param int $size The size of the video file in bytes. * * @return array{0: int, 1: int}|never The start and end of the range. + * + * @SuppressWarnings(PHPMD.ExitExpression) */ private static function resolve_range( $size ) { if ( ! isset( $_SERVER['HTTP_RANGE'] ) ) { @@ -113,9 +117,9 @@ private static function resolve_range( $size ) { die(); } $raw_start = $limits[0]; - $raw_end = $limits[1]; - $start = is_numeric( $raw_start ) ? intval( $raw_start ) : 0; - $end = is_numeric( $raw_end ) ? intval( $raw_end ) : $size - 1; + $raw_end = $limits[1]; + $start = is_numeric( $raw_start ) ? intval( $raw_start ) : 0; + $end = is_numeric( $raw_end ) ? intval( $raw_end ) : $size - 1; if ( $start < 0 ) { $start = 0; } From 887a5e311946d6e7668ac42fedcef5e09b25bb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 15:44:24 +0200 Subject: [PATCH 21/29] Split out Video_Proxy::get_range_header() --- src/php/frontend/class-video-proxy.php | 38 ++++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 57cc180db..ba02bf1a4 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -99,18 +99,7 @@ private static function resolve_range( $size ) { if ( ! isset( $_SERVER['HTTP_RANGE'] ) ) { return array( 0, $size - 1 ); } - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $header = sanitize_text_field( wp_unslash( strval( $_SERVER['HTTP_RANGE'] ) ) ); - if ( ! str_starts_with( $header, 'bytes=' ) ) { - http_response_code( 416 ); - die(); - } - $header = substr( $header, 6 ); - // Multipart range requests are not supported. - if ( str_contains( $header, ',' ) ) { - http_response_code( 416 ); - die(); - } + $header = self::get_range_header(); $limits = explode( '-', $header ); if ( 2 !== count( $limits ) ) { http_response_code( 416 ); @@ -132,4 +121,29 @@ private static function resolve_range( $size ) { } return array( $start, $end ); } + + /** + * Returns the contents of the HTTP Range header. + * + * This function assumes that the header is present + * + * @return string The byte range from the header. + * + * @SuppressWarnings(PHPMD.ExitExpression) + */ + private static function get_range_header() { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $header = sanitize_text_field( wp_unslash( strval( $_SERVER['HTTP_RANGE'] ) ) ); + if ( ! str_starts_with( $header, 'bytes=' ) ) { + http_response_code( 416 ); + die(); + } + $header = substr( $header, 6 ); + // Multipart range requests are not supported. + if ( str_contains( $header, ',' ) ) { + http_response_code( 416 ); + die(); + } + return $header; + } } From 95c4ec960da4011b9033eed947fd57c36bb7dc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 22:48:06 +0200 Subject: [PATCH 22/29] Removed TODO that was remade into #1517 --- src/php/frontend/class-video-proxy.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index ba02bf1a4..1384ebedb 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -62,7 +62,6 @@ public static function ajax_handler_body() { http_response_code( 206 ); $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); - // TODO: X-Goog-Drive-Resource-Keys header? $response = $http->request( 'GET', 'drive/v3/files/' . $transient['id'], From 3c288ca5c3ac6f4d0d186f7ffdf98bef62c6c952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 19 Jul 2022 23:11:05 +0200 Subject: [PATCH 23/29] Split the Page class --- src/php/frontend/class-page.php | 378 +------------------- src/php/frontend/page/class-directories.php | 144 ++++++++ src/php/frontend/page/class-images.php | 114 ++++++ src/php/frontend/page/class-videos.php | 153 ++++++++ src/php/skaut-google-drive-gallery.php | 4 + 5 files changed, 418 insertions(+), 375 deletions(-) create mode 100644 src/php/frontend/page/class-directories.php create mode 100644 src/php/frontend/page/class-images.php create mode 100644 src/php/frontend/page/class-videos.php diff --git a/src/php/frontend/class-page.php b/src/php/frontend/class-page.php index 3c75d8e30..a5160c028 100644 --- a/src/php/frontend/class-page.php +++ b/src/php/frontend/class-page.php @@ -68,19 +68,19 @@ static function( $page ) { */ public static function get_page( $parent_id, $pagination_helper, $options ) { $page = array( - 'directories' => self::directories( $parent_id, $pagination_helper, $options ), + 'directories' => Page\Directories::directories( $parent_id, $pagination_helper, $options ), ); return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( $page )->then( static function( $page ) use ( $parent_id, $pagination_helper, $options ) { if ( $pagination_helper->should_continue() ) { - $page['images'] = self::images( $parent_id, $pagination_helper, $options ); + $page['images'] = Page\Images::images( $parent_id, $pagination_helper, $options ); } return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( $page ); } )->then( static function( $page ) use ( $parent_id, $pagination_helper, $options ) { if ( $pagination_helper->should_continue() ) { - $page['videos'] = self::videos( $parent_id, $pagination_helper, $options ); + $page['videos'] = Page\Videos::videos( $parent_id, $pagination_helper, $options ); } return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( $page ); } @@ -91,376 +91,4 @@ static function( $page ) use ( $pagination_helper ) { } ); } - - /** - * Returns a list of subdirectories in a directory. - * - * @param string $parent_id A directory to list items of. - * @param \Sgdg\Frontend\Pagination_Helper $pagination_helper An initialized pagination helper. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of directories in the format `['id' =>, 'id', 'name' => 'name', 'thumbnail' => 'thumbnail', 'dircount' => 1, 'imagecount' => 1]`. - */ - private static function directories( $parent_id, $pagination_helper, $options ) { - return ( \Sgdg\API_Facade::list_directories( $parent_id, new \Sgdg\Frontend\API_Fields( array( 'id', 'name' ) ), $options->get( 'dir_ordering' ), $pagination_helper )->then( - static function( $files ) use ( &$options ) { - $files = array_map( - static function( $file ) use ( &$options ) { - if ( '' !== $options->get( 'dir_prefix' ) ) { - $pos = mb_strpos( $file['name'], $options->get( 'dir_prefix' ) ); - $file['name'] = mb_substr( $file['name'], false !== $pos ? $pos + 1 : 0 ); - } - return $file; - }, - $files - ); - $ids = array_column( $files, 'id' ); - - return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( array( $files, self::dir_images( $ids, $options ), self::dir_counts( $ids ) ) ); - } - )->then( - static function( $list ) use ( &$options ) { - list( $files, $images, $counts ) = $list; - $count = count( $files ); - for ( $i = 0; $i < $count; $i++ ) { - $files[ $i ]['thumbnail'] = $images[ $i ]; - if ( 'true' === $options->get( 'dir_counts' ) ) { - $files[ $i ] = array_merge( $files[ $i ], $counts[ $i ] ); - } - if ( 0 === $counts[ $i ]['dircount'] + $counts[ $i ]['imagecount'] + $counts[ $i ]['videocount'] ) { - unset( $files[ $i ] ); - } - } - return array_values( $files ); // Needed because of the unset not re-indexing. - } - ) ); - } - - /** - * Creates API requests for directory thumbnails - * - * Takes a batch and adds to it a request for the first image in each directory. - * - * @param array $dirs A list of directory IDs. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of directory images - */ - private static function dir_images( $dirs, $options ) { - return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( - array_map( - static function( $directory ) use ( &$options ) { - return \Sgdg\API_Facade::list_images( - $directory, - new \Sgdg\Frontend\API_Fields( - array( - 'imageMediaMetadata' => array( 'width', 'height' ), - 'thumbnailLink', - ) - ), - $options->get( 'image_ordering' ), - ( new \Sgdg\Frontend\Pagination_Helper() )->withValues( 0, 1 ) - )->then( - static function( $images ) use ( &$options ) { - if ( count( $images ) === 0 ) { - return false; - } - return substr( $images[0]['thumbnailLink'], 0, -4 ) . ( $images[0]['imageMediaMetadata']['width'] > $images[0]['imageMediaMetadata']['height'] ? 'h' : 'w' ) . floor( 1.25 * $options->get( 'grid_height' ) ); - } - ); - }, - $dirs - ) - ); - } - - /** - * Creates API requests for directory item counts - * - * Takes a batch and adds to it requests for the counts of subdirectories and images for each directory. - * - * @param array $dirs A list of directory IDs. - * - * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of subdirectory, image and video counts of format `['dircount' => 1, 'imagecount' => 1, 'videocount' => 1]` for each directory. - */ - private static function dir_counts( $dirs ) { - return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( - array_map( - static function( $dir ) { - return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( - array( - \Sgdg\API_Facade::list_directories( - $dir, - new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), - 'name', - new \Sgdg\Frontend\Single_Page_Pagination_Helper() - ), - \Sgdg\API_Facade::list_images( - $dir, - new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), - 'name', - new \Sgdg\Frontend\Single_Page_Pagination_Helper() - ), - \Sgdg\API_Facade::list_videos( - $dir, - new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), - 'name', - new \Sgdg\Frontend\Single_Page_Pagination_Helper() - ), - ) - )->then( - static function( $items ) { - return array( - 'dircount' => count( $items[0] ), - 'imagecount' => count( $items[1] ), - 'videocount' => count( $items[2] ), - ); - } - ); - }, - $dirs - ) - ); - } - - /** - * Returns a list of images in a directory - * - * @param string $parent_id A directory to list items of. - * @param \Sgdg\Frontend\Pagination_Helper $pagination_helper An initialized pagination helper. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of images in the format `['id' =>, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail']`. - */ - private static function images( $parent_id, $pagination_helper, $options ) { - if ( $options->get_by( 'image_ordering' ) === 'time' ) { - $order_by = 'name'; - $fields = new \Sgdg\Frontend\API_Fields( - array( - 'id', - 'thumbnailLink', - 'createdTime', - 'imageMediaMetadata' => array( 'time' ), - 'description', - ) - ); - } else { - $order_by = $options->get( 'image_ordering' ); - $fields = new \Sgdg\Frontend\API_Fields( array( 'id', 'thumbnailLink', 'description' ) ); - } - return \Sgdg\API_Facade::list_images( $parent_id, $fields, $order_by, $pagination_helper )->then( - static function( $images ) use ( &$options ) { - $images = array_map( - static function( $image ) use ( &$options ) { - return self::image_preprocess( $image, $options ); - }, - $images - ); - return self::images_order( $images, $options ); - } - ); - } - - /** - * Processes an image response. - * - * @param array $image An image. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return array{id: string, description: string, image: string, thumbnail: string, timestamp?: int} { - * @type string $id The ID of the image. - * @type string $description The description (caption) of the image. - * @type string $image A URL of the image to be displayed in the lightbox - * @type string $thumbnail A URL of a thumbnail to be displayed in the image grid. - * @type int|null $timestamp A timestamp to order the images by. Optional. - * } - */ - private static function image_preprocess( $image, $options ) { - $ret = array( - 'id' => $image['id'], - 'description' => array_key_exists( 'description', $image ) ? esc_attr( $image['description'] ) : '', - 'image' => substr( $image['thumbnailLink'], 0, -3 ) . $options->get( 'preview_size' ), - 'thumbnail' => substr( $image['thumbnailLink'], 0, -4 ) . 'h' . floor( 1.25 * $options->get( 'grid_height' ) ), - ); - if ( $options->get_by( 'image_ordering' ) === 'time' ) { - if ( array_key_exists( 'imageMediaMetadata', $image ) && array_key_exists( 'time', $image['imageMediaMetadata'] ) ) { - $timestamp = \DateTime::createFromFormat( 'Y:m:d H:i:s', $image['imageMediaMetadata']['time'] ); - } else { - $timestamp = \DateTime::createFromFormat( 'Y-m-d\TH:i:s.uP', $image['createdTime'] ); - } - if ( false !== $timestamp ) { - $ret['timestamp'] = intval( $timestamp->format( 'U' ) ); - } - } - return $ret; - } - - /** - * Orders images. - * - * @param array $images A list of images in the format `['id' =>, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail', 'timestamp' => 1638012797]`. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return array An ordered list of images in the format `['id' =>, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail']`. - */ - private static function images_order( $images, $options ) { - if ( $options->get_by( 'image_ordering' ) === 'time' ) { - usort( - $images, - static function( $first, $second ) use ( $options ) { - $first_timestamp = array_key_exists( 'timestamp', $first ) ? $first['timestamp'] : time(); - $second_timestamp = array_key_exists( 'timestamp', $second ) ? $second['timestamp'] : time(); - $asc = $first_timestamp - $second_timestamp; - return $options->get_order( 'image_ordering' ) === 'ascending' ? $asc : -$asc; - } - ); - array_walk( - $images, - static function( &$item ) { - unset( $item['timestamp'] ); - } - ); - } - return $images; - } - - /** - * Returns a list of videos in a directory - * - * @param string $parent_id A directory to list items of. - * @param \Sgdg\Frontend\Pagination_Helper $pagination_helper An initialized pagination helper. - * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. - * - * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of videos in the format `['id' =>, 'id', 'thumbnail' => 'thumbnail', 'mimeType' => 'mimeType', 'src' => 'src']`. - */ - private static function videos( $parent_id, $pagination_helper, $options ) { - return \Sgdg\API_Facade::list_videos( - $parent_id, - new \Sgdg\Frontend\API_Fields( - array( - 'id', - 'mimeType', - 'size', - 'webContentLink', - 'webViewLink', - 'thumbnailLink', - 'videoMediaMetadata' => array( 'width', 'height' ), - 'copyRequiresWriterPermission', - 'permissions' => array( 'type', 'role' ), - ) - ), - $options->get( 'image_ordering' ), - $pagination_helper - )->then( - static function( $videos ) use ( &$options ) { - return array_map( - static function( $video ) use ( &$options ) { - return array( - 'id' => $video['id'], - 'thumbnail' => substr( $video['thumbnailLink'], 0, -4 ) . 'h' . floor( 1.25 * $options->get( 'grid_height' ) ), - 'mimeType' => $video['mimeType'], - 'width' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'width', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['width'] : '0', - 'height' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'height', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], array_key_exists( 'permissions', $video ) ? $video['permissions'] : array() ), - ); - }, - $videos - ); - } - ); - } - - /** - * Resolves the correct URL for a video. - * - * Finds the correct URL so that a video would load in the browser. - * - * @param string $video_id The ID of the video. - * @param string $mime_type The MIME type of the video. - * @param int $size The size of the video in bytes. - * @param string $web_content_url The webContentLink returned by Google Drive API. - * @param string $web_view_url The webViewLink returned by Google Drive API. - * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. - * @param array $permissions The file permissions. - * - * @return string The resolved video URL. - * - * @SuppressWarnings(PHPMD.LongVariable) - */ - private static function resolve_video_url( $video_id, $mime_type, $size, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { - if ( $copy_requires_writer_permission || $size > 25165824 ) { - return self::get_proxy_video_url( $video_id, $mime_type, $size ); - } - foreach ( $permissions as $permission ) { - if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { - return self::get_direct_video_url( $web_content_url ); - } - } - $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - $response = $http_client->get( $web_view_url, array( 'allow_redirects' => false ) ); // TODO: Use promises? - if ( 200 === $response->getStatusCode() ) { - return self::get_direct_video_url( $web_content_url ); - } - return self::get_proxy_video_url( $video_id, $mime_type, $size ); - } - - /** - * Returns the direct URL for a video. - * - * Goes through the download warning and returns the direct download URL for a video. - * - * @param string $web_content_url The webContentLink returned by Google Drive API. - * - * @return string The resolved video URL. - */ - private static function get_direct_video_url( $web_content_url ) { - $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - $url = $web_content_url; - $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? - - if ( $response->hasHeader( 'Set-Cookie' ) && 0 === mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { - // Handle virus scan warning. - mb_ereg( '(download_warning[^=]*)=([^;]*).*Domain=([^;]*)', $response->getHeader( 'Set-Cookie' )[0], $regs ); - $name = $regs[1]; - $confirm = $regs[2]; - $domain = $regs[3]; - $cookie_jar = \Sgdg\Vendor\GuzzleHttp\Cookie\CookieJar::fromArray( array( $name => $confirm ), $domain ); - - $response = $http_client->head( - $url . '&confirm=' . $confirm, - array( - 'allow_redirects' => false, - 'cookies' => $cookie_jar, - ) - ); - $url = $response->getHeader( 'Location' )[0]; - } - return $url; - } - - /** - * Returns the proxy URL for a video. - * - * Sets up a proxy in WordPress and returns the address of this proxy. - * - * @param string $video_id The ID of the video. - * @param string $mime_type The MIME type of the video. - * @param int $size The size of the video in bytes. - * - * @return string The resolved video URL. - */ - private static function get_proxy_video_url( $video_id, $mime_type, $size ) { - $gallery_hash = \Sgdg\GET_Helpers::get_string_variable( 'hash' ); - $video_hash = hash( 'sha256', $gallery_hash . $video_id ); - set_transient( - 'sgdg_video_proxy_' . $video_hash, - array( - 'id' => $video_id, - 'mimeType' => $mime_type, - 'size' => $size, - ), - DAY_IN_SECONDS - ); - return admin_url( 'admin-ajax.php?action=video_proxy&video_hash=' . $video_hash ); - } } diff --git a/src/php/frontend/page/class-directories.php b/src/php/frontend/page/class-directories.php new file mode 100644 index 000000000..064895319 --- /dev/null +++ b/src/php/frontend/page/class-directories.php @@ -0,0 +1,144 @@ +, 'id', 'name' => 'name', 'thumbnail' => 'thumbnail', 'dircount' => 1, 'imagecount' => 1, 'videocount' => 1]`. + */ + public static function directories( $parent_id, $pagination_helper, $options ) { + return ( \Sgdg\API_Facade::list_directories( $parent_id, new \Sgdg\Frontend\API_Fields( array( 'id', 'name' ) ), $options->get( 'dir_ordering' ), $pagination_helper )->then( + static function( $files ) use ( &$options ) { + $files = array_map( + static function( $file ) use ( &$options ) { + if ( '' !== $options->get( 'dir_prefix' ) ) { + $pos = mb_strpos( $file['name'], $options->get( 'dir_prefix' ) ); + $file['name'] = mb_substr( $file['name'], false !== $pos ? $pos + 1 : 0 ); + } + return $file; + }, + $files + ); + $ids = array_column( $files, 'id' ); + + return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( array( $files, self::thumbnail_images( $ids, $options ), self::directory_item_counts( $ids ) ) ); + } + )->then( + static function( $list ) use ( &$options ) { + list( $files, $images, $counts ) = $list; + $count = count( $files ); + for ( $i = 0; $i < $count; $i++ ) { + $files[ $i ]['thumbnail'] = $images[ $i ]; + if ( 'true' === $options->get( 'dir_counts' ) ) { + $files[ $i ] = array_merge( $files[ $i ], $counts[ $i ] ); + } + if ( 0 === $counts[ $i ]['dircount'] + $counts[ $i ]['imagecount'] + $counts[ $i ]['videocount'] ) { + unset( $files[ $i ] ); + } + } + return array_values( $files ); // Needed because of the unset not re-indexing. + } + ) ); + } + + /** + * Creates API requests for directory thumbnails + * + * Takes a batch and adds to it a request for the first image in each directory. + * + * @param array $dirs A list of directory IDs. + * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. + * + * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of directory images. + */ + private static function thumbnail_images( $dirs, $options ) { + return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( + array_map( + static function( $directory ) use ( &$options ) { + return \Sgdg\API_Facade::list_images( + $directory, + new \Sgdg\Frontend\API_Fields( + array( + 'imageMediaMetadata' => array( 'width', 'height' ), + 'thumbnailLink', + ) + ), + $options->get( 'image_ordering' ), + ( new \Sgdg\Frontend\Pagination_Helper() )->withValues( 0, 1 ) + )->then( + static function( $images ) use ( &$options ) { + if ( count( $images ) === 0 ) { + return false; + } + return substr( $images[0]['thumbnailLink'], 0, -4 ) . ( $images[0]['imageMediaMetadata']['width'] > $images[0]['imageMediaMetadata']['height'] ? 'h' : 'w' ) . floor( 1.25 * $options->get( 'grid_height' ) ); + } + ); + }, + $dirs + ) + ); + } + + /** + * Creates API requests for directory item counts + * + * Takes a batch and adds to it requests for the counts of subdirectories and images for each directory. + * + * @param array $dirs A list of directory IDs. + * + * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to a list of subdirectory, image and video counts of format `['dircount' => 1, 'imagecount' => 1, 'videocount' => 1]` for each directory. + */ + private static function directory_item_counts( $dirs ) { + return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( + array_map( + static function( $dir ) { + return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( + array( + \Sgdg\API_Facade::list_directories( + $dir, + new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), + 'name', + new \Sgdg\Frontend\Single_Page_Pagination_Helper() + ), + \Sgdg\API_Facade::list_images( + $dir, + new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), + 'name', + new \Sgdg\Frontend\Single_Page_Pagination_Helper() + ), + \Sgdg\API_Facade::list_videos( + $dir, + new \Sgdg\Frontend\API_Fields( array( 'createdTime' ) ), + 'name', + new \Sgdg\Frontend\Single_Page_Pagination_Helper() + ), + ) + )->then( + static function( $items ) { + return array( + 'dircount' => count( $items[0] ), + 'imagecount' => count( $items[1] ), + 'videocount' => count( $items[2] ), + ); + } + ); + }, + $dirs + ) + ); + } +} diff --git a/src/php/frontend/page/class-images.php b/src/php/frontend/page/class-images.php new file mode 100644 index 000000000..16fb3ef74 --- /dev/null +++ b/src/php/frontend/page/class-images.php @@ -0,0 +1,114 @@ +, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail']`. + */ + public static function images( $parent_id, $pagination_helper, $options ) { + if ( $options->get_by( 'image_ordering' ) === 'time' ) { + $order_by = 'name'; + $fields = new \Sgdg\Frontend\API_Fields( + array( + 'id', + 'thumbnailLink', + 'createdTime', + 'imageMediaMetadata' => array( 'time' ), + 'description', + ) + ); + } else { + $order_by = $options->get( 'image_ordering' ); + $fields = new \Sgdg\Frontend\API_Fields( array( 'id', 'thumbnailLink', 'description' ) ); + } + return \Sgdg\API_Facade::list_images( $parent_id, $fields, $order_by, $pagination_helper )->then( + static function( $images ) use ( &$options ) { + $images = array_map( + static function( $image ) use ( &$options ) { + return self::image_preprocess( $image, $options ); + }, + $images + ); + return self::images_order( $images, $options ); + } + ); + } + + /** + * Processes an image response. + * + * @param array $image An image. + * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. + * + * @return array{id: string, description: string, image: string, thumbnail: string, timestamp?: int} { + * @type string $id The ID of the image. + * @type string $description The description (caption) of the image. + * @type string $image A URL of the image to be displayed in the lightbox + * @type string $thumbnail A URL of a thumbnail to be displayed in the image grid. + * @type int|null $timestamp A timestamp to order the images by. Optional. + * } + */ + private static function image_preprocess( $image, $options ) { + $ret = array( + 'id' => $image['id'], + 'description' => array_key_exists( 'description', $image ) ? esc_attr( $image['description'] ) : '', + 'image' => substr( $image['thumbnailLink'], 0, -3 ) . $options->get( 'preview_size' ), + 'thumbnail' => substr( $image['thumbnailLink'], 0, -4 ) . 'h' . floor( 1.25 * $options->get( 'grid_height' ) ), + ); + if ( $options->get_by( 'image_ordering' ) === 'time' ) { + if ( array_key_exists( 'imageMediaMetadata', $image ) && array_key_exists( 'time', $image['imageMediaMetadata'] ) ) { + $timestamp = \DateTime::createFromFormat( 'Y:m:d H:i:s', $image['imageMediaMetadata']['time'] ); + } else { + $timestamp = \DateTime::createFromFormat( 'Y-m-d\TH:i:s.uP', $image['createdTime'] ); + } + if ( false !== $timestamp ) { + $ret['timestamp'] = intval( $timestamp->format( 'U' ) ); + } + } + return $ret; + } + + /** + * Orders images. + * + * @param array $images A list of images in the format `['id' =>, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail', 'timestamp' => 1638012797]`. + * @param \Sgdg\Frontend\Options_Proxy $options The configuration of the gallery. + * + * @return array An ordered list of images in the format `['id' =>, 'id', 'description' => 'description', 'image' => 'image', 'thumbnail' => 'thumbnail']`. + */ + private static function images_order( $images, $options ) { + if ( $options->get_by( 'image_ordering' ) === 'time' ) { + usort( + $images, + static function( $first, $second ) use ( $options ) { + $first_timestamp = array_key_exists( 'timestamp', $first ) ? $first['timestamp'] : time(); + $second_timestamp = array_key_exists( 'timestamp', $second ) ? $second['timestamp'] : time(); + $asc = $first_timestamp - $second_timestamp; + return $options->get_order( 'image_ordering' ) === 'ascending' ? $asc : -$asc; + } + ); + array_walk( + $images, + static function( &$item ) { + unset( $item['timestamp'] ); + } + ); + } + return $images; + } +} diff --git a/src/php/frontend/page/class-videos.php b/src/php/frontend/page/class-videos.php new file mode 100644 index 000000000..c7bd07cfa --- /dev/null +++ b/src/php/frontend/page/class-videos.php @@ -0,0 +1,153 @@ +, 'id', 'thumbnail' => 'thumbnail', 'mimeType' => 'mimeType', 'src' => 'src']`. + */ + public static function videos( $parent_id, $pagination_helper, $options ) { + return \Sgdg\API_Facade::list_videos( + $parent_id, + new \Sgdg\Frontend\API_Fields( + array( + 'id', + 'mimeType', + 'size', + 'webContentLink', + 'webViewLink', + 'thumbnailLink', + 'videoMediaMetadata' => array( 'width', 'height' ), + 'copyRequiresWriterPermission', + 'permissions' => array( 'type', 'role' ), + ) + ), + $options->get( 'image_ordering' ), + $pagination_helper + )->then( + static function( $videos ) use ( &$options ) { + return array_map( + static function( $video ) use ( &$options ) { + return array( + 'id' => $video['id'], + 'thumbnail' => substr( $video['thumbnailLink'], 0, -4 ) . 'h' . floor( 1.25 * $options->get( 'grid_height' ) ), + 'mimeType' => $video['mimeType'], + 'width' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'width', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['width'] : '0', + 'height' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'height', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['height'] : '0', + 'src' => self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], array_key_exists( 'permissions', $video ) ? $video['permissions'] : array() ), + ); + }, + $videos + ); + } + ); + } + + /** + * Resolves the correct URL for a video. + * + * Finds the correct URL so that a video would load in the browser. + * + * @param string $video_id The ID of the video. + * @param string $mime_type The MIME type of the video. + * @param int $size The size of the video in bytes. + * @param string $web_content_url The webContentLink returned by Google Drive API. + * @param string $web_view_url The webViewLink returned by Google Drive API. + * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. + * @param array $permissions The file permissions. + * + * @return string The resolved video URL. + * + * @SuppressWarnings(PHPMD.LongVariable) + */ + private static function resolve_video_url( $video_id, $mime_type, $size, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { + if ( $copy_requires_writer_permission || $size > 25165824 ) { + return self::get_proxy_video_url( $video_id, $mime_type, $size ); + } + foreach ( $permissions as $permission ) { + if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { + return self::get_direct_video_url( $web_content_url ); + } + } + $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); + $response = $http_client->get( $web_view_url, array( 'allow_redirects' => false ) ); // TODO: Use promises? + if ( 200 === $response->getStatusCode() ) { + return self::get_direct_video_url( $web_content_url ); + } + return self::get_proxy_video_url( $video_id, $mime_type, $size ); + } + + /** + * Returns the direct URL for a video. + * + * Goes through the download warning and returns the direct download URL for a video. + * + * @param string $web_content_url The webContentLink returned by Google Drive API. + * + * @return string The resolved video URL. + */ + private static function get_direct_video_url( $web_content_url ) { + $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); + $url = $web_content_url; + $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? + + if ( $response->hasHeader( 'Set-Cookie' ) && 0 === mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { + // Handle virus scan warning. + mb_ereg( '(download_warning[^=]*)=([^;]*).*Domain=([^;]*)', $response->getHeader( 'Set-Cookie' )[0], $regs ); + $name = $regs[1]; + $confirm = $regs[2]; + $domain = $regs[3]; + $cookie_jar = \Sgdg\Vendor\GuzzleHttp\Cookie\CookieJar::fromArray( array( $name => $confirm ), $domain ); + + $response = $http_client->head( + $url . '&confirm=' . $confirm, + array( + 'allow_redirects' => false, + 'cookies' => $cookie_jar, + ) + ); + $url = $response->getHeader( 'Location' )[0]; + } + return $url; + } + + /** + * Returns the proxy URL for a video. + * + * Sets up a proxy in WordPress and returns the address of this proxy. + * + * @param string $video_id The ID of the video. + * @param string $mime_type The MIME type of the video. + * @param int $size The size of the video in bytes. + * + * @return string The resolved video URL. + */ + private static function get_proxy_video_url( $video_id, $mime_type, $size ) { + $gallery_hash = \Sgdg\GET_Helpers::get_string_variable( 'hash' ); + $video_hash = hash( 'sha256', $gallery_hash . $video_id ); + set_transient( + 'sgdg_video_proxy_' . $video_hash, + array( + 'id' => $video_id, + 'mimeType' => $mime_type, + 'size' => $size, + ), + DAY_IN_SECONDS + ); + return admin_url( 'admin-ajax.php?action=video_proxy&video_hash=' . $video_hash ); + } +} diff --git a/src/php/skaut-google-drive-gallery.php b/src/php/skaut-google-drive-gallery.php index a7ce04dd1..6ad18ab29 100644 --- a/src/php/skaut-google-drive-gallery.php +++ b/src/php/skaut-google-drive-gallery.php @@ -73,6 +73,10 @@ require_once __DIR__ . '/helpers/class-helpers.php'; require_once __DIR__ . '/helpers/class-script-and-style-helpers.php'; +require_once __DIR__ . '/frontend/page/class-directories.php'; +require_once __DIR__ . '/frontend/page/class-images.php'; +require_once __DIR__ . '/frontend/page/class-videos.php'; + require_once __DIR__ . '/frontend/interface-pagination-helper-interface.php'; require_once __DIR__ . '/frontend/class-api-fields.php'; require_once __DIR__ . '/frontend/class-block.php'; From b6f51e184dd17cc43cd516abe3a418dec13cbb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 00:09:56 +0200 Subject: [PATCH 24/29] Lint fixes --- src/php/frontend/class-video-proxy.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 1384ebedb..61f19db1d 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -61,7 +61,7 @@ public static function ajax_handler_body() { header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $transient['size'] ); http_response_code( 206 ); - $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); + $http = \Sgdg\API_Client::get_authorized_raw_client()->authorize(); $response = $http->request( 'GET', 'drive/v3/files/' . $transient['id'], @@ -98,7 +98,8 @@ private static function resolve_range( $size ) { if ( ! isset( $_SERVER['HTTP_RANGE'] ) ) { return array( 0, $size - 1 ); } - $header = self::get_range_header(); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $header = self::check_range_header( sanitize_text_field( wp_unslash( strval( $_SERVER['HTTP_RANGE'] ) ) ) ); $limits = explode( '-', $header ); if ( 2 !== count( $limits ) ) { http_response_code( 416 ); @@ -124,15 +125,13 @@ private static function resolve_range( $size ) { /** * Returns the contents of the HTTP Range header. * - * This function assumes that the header is present + * @param string $header The raw header. * * @return string The byte range from the header. * * @SuppressWarnings(PHPMD.ExitExpression) */ - private static function get_range_header() { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $header = sanitize_text_field( wp_unslash( strval( $_SERVER['HTTP_RANGE'] ) ) ); + private static function check_range_header( $header ) { if ( ! str_starts_with( $header, 'bytes=' ) ) { http_response_code( 416 ); die(); From cba5a83ad9b80c8c6ccb834518869b83b75c42a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 00:21:12 +0200 Subject: [PATCH 25/29] Fixed Content-Length header for video proxying --- src/php/frontend/class-video-proxy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php/frontend/class-video-proxy.php b/src/php/frontend/class-video-proxy.php index 61f19db1d..b938c73f0 100644 --- a/src/php/frontend/class-video-proxy.php +++ b/src/php/frontend/class-video-proxy.php @@ -54,10 +54,10 @@ public static function ajax_handler_body() { header( 'Accept-Ranges: bytes' ); header( 'Content-Disposition: attachment' ); - header( 'Content-Length: ' . $transient['size'] ); header( 'Content-Type: ' . $transient['mimeType'] ); // The headers above should be set before the call to `resolve_range()` so that they are present even if the range request fails. list( $start, $end ) = self::resolve_range( $transient['size'] ); + header( 'Content-Length: ' . ( $end - $start + 1 ) ); header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $transient['size'] ); http_response_code( 206 ); From 2e7091645bf630ba6d585a156db48ce98651f8fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 00:45:47 +0200 Subject: [PATCH 26/29] Enbaled the use of promises in resolve_video_url --- src/php/frontend/page/class-videos.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/php/frontend/page/class-videos.php b/src/php/frontend/page/class-videos.php index c7bd07cfa..b7fb424ca 100644 --- a/src/php/frontend/page/class-videos.php +++ b/src/php/frontend/page/class-videos.php @@ -39,8 +39,8 @@ public static function videos( $parent_id, $pagination_helper, $options ) { $options->get( 'image_ordering' ), $pagination_helper )->then( - static function( $videos ) use ( &$options ) { - return array_map( + static function( $raw_videos ) use ( &$options ) { + $videos = array_map( static function( $video ) use ( &$options ) { return array( 'id' => $video['id'], @@ -48,11 +48,25 @@ static function( $video ) use ( &$options ) { 'mimeType' => $video['mimeType'], 'width' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'width', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['width'] : '0', 'height' => array_key_exists( 'videoMediaMetadata', $video ) && array_key_exists( 'height', $video['videoMediaMetadata'] ) ? $video['videoMediaMetadata']['height'] : '0', - 'src' => self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], array_key_exists( 'permissions', $video ) ? $video['permissions'] : array() ), ); }, - $videos + $raw_videos ); + $video_url_promises = array_map( + static function( $video ) { + return self::resolve_video_url( $video['id'], $video['mimeType'], $video['size'], $video['webContentLink'], $video['webViewLink'], $video['copyRequiresWriterPermission'], array_key_exists( 'permissions', $video ) ? $video['permissions'] : array() ); + }, + $raw_videos + ); + return \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( array( $videos, \Sgdg\Vendor\GuzzleHttp\Promise\Utils::all( $video_url_promises ) ) ); + } + )->then( + static function( $list ) { + list( $videos, $video_urls ) = $list; + for( $i = 0; $i < count( $videos ); $i++ ) { + $videos[ $i ]['src'] = $video_urls[ $i ]; + } + return $videos; } ); } From de39945052f2c4a733dc0145742e44cd4e519617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 00:52:32 +0200 Subject: [PATCH 27/29] Async request in resolve_video_url --- src/php/frontend/page/class-videos.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/php/frontend/page/class-videos.php b/src/php/frontend/page/class-videos.php index b7fb424ca..59d92e075 100644 --- a/src/php/frontend/page/class-videos.php +++ b/src/php/frontend/page/class-videos.php @@ -84,25 +84,28 @@ static function( $list ) { * @param bool $copy_requires_writer_permission Whether the option to download the file is disabled for readers. * @param array $permissions The file permissions. * - * @return string The resolved video URL. + * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to the video URL. * * @SuppressWarnings(PHPMD.LongVariable) */ private static function resolve_video_url( $video_id, $mime_type, $size, $web_content_url, $web_view_url, $copy_requires_writer_permission, $permissions ) { if ( $copy_requires_writer_permission || $size > 25165824 ) { - return self::get_proxy_video_url( $video_id, $mime_type, $size ); + return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_proxy_video_url( $video_id, $mime_type, $size ) ); } foreach ( $permissions as $permission ) { if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { - return self::get_direct_video_url( $web_content_url ); + return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_direct_video_url( $web_content_url ) ); } } $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - $response = $http_client->get( $web_view_url, array( 'allow_redirects' => false ) ); // TODO: Use promises? - if ( 200 === $response->getStatusCode() ) { - return self::get_direct_video_url( $web_content_url ); - } - return self::get_proxy_video_url( $video_id, $mime_type, $size ); + return $http_client->getAsync( $web_view_url, array( 'allow_redirects' => false ) )->then( + static function( $response ) use ( $video_id, $mime_type, $size, $web_content_url ) { + if ( 200 === $response->getStatusCode() ) { + return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_direct_video_url( $web_content_url ) ); + } + return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_proxy_video_url( $video_id, $mime_type, $size ) ); + } + ); } /** From a38a33f2b04dd7c016a302f7e1da5059e151d0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 01:01:23 +0200 Subject: [PATCH 28/29] Async request in get_direct_video_url --- src/php/frontend/page/class-videos.php | 51 ++++++++++++++------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/php/frontend/page/class-videos.php b/src/php/frontend/page/class-videos.php index 59d92e075..e83be4bb3 100644 --- a/src/php/frontend/page/class-videos.php +++ b/src/php/frontend/page/class-videos.php @@ -94,14 +94,14 @@ private static function resolve_video_url( $video_id, $mime_type, $size, $web_co } foreach ( $permissions as $permission ) { if ( 'anyone' === $permission['type'] && in_array( $permission['role'], array( 'reader', 'writer' ), true ) ) { - return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_direct_video_url( $web_content_url ) ); + return self::get_direct_video_url( $web_content_url ); } } $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); return $http_client->getAsync( $web_view_url, array( 'allow_redirects' => false ) )->then( static function( $response ) use ( $video_id, $mime_type, $size, $web_content_url ) { if ( 200 === $response->getStatusCode() ) { - return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_direct_video_url( $web_content_url ) ); + return self::get_direct_video_url( $web_content_url ); } return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( self::get_proxy_video_url( $video_id, $mime_type, $size ) ); } @@ -115,31 +115,36 @@ static function( $response ) use ( $video_id, $mime_type, $size, $web_content_ur * * @param string $web_content_url The webContentLink returned by Google Drive API. * - * @return string The resolved video URL. + * @return \Sgdg\Vendor\GuzzleHttp\Promise\PromiseInterface A promise resolving to the video URL. */ private static function get_direct_video_url( $web_content_url ) { $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - $url = $web_content_url; - $response = $http_client->get( $url, array( 'allow_redirects' => false ) ); // TODO: Use promises? - - if ( $response->hasHeader( 'Set-Cookie' ) && 0 === mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { - // Handle virus scan warning. - mb_ereg( '(download_warning[^=]*)=([^;]*).*Domain=([^;]*)', $response->getHeader( 'Set-Cookie' )[0], $regs ); - $name = $regs[1]; - $confirm = $regs[2]; - $domain = $regs[3]; - $cookie_jar = \Sgdg\Vendor\GuzzleHttp\Cookie\CookieJar::fromArray( array( $name => $confirm ), $domain ); + return $http_client->getAsync( $web_content_url, array( 'allow_redirects' => false ) )->then( + static function( $response ) use ( $http_client, $web_content_url ) { + $url = $web_content_url; + if ( ! $response->hasHeader( 'Set-Cookie' ) || 0 !== mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { + return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( $web_content_url ); + } + // Handle virus scan warning. + mb_ereg( '(download_warning[^=]*)=([^;]*).*Domain=([^;]*)', $response->getHeader( 'Set-Cookie' )[0], $regs ); + $name = $regs[1]; + $confirm = $regs[2]; + $domain = $regs[3]; + $cookie_jar = \Sgdg\Vendor\GuzzleHttp\Cookie\CookieJar::fromArray( array( $name => $confirm ), $domain ); - $response = $http_client->head( - $url . '&confirm=' . $confirm, - array( - 'allow_redirects' => false, - 'cookies' => $cookie_jar, - ) - ); - $url = $response->getHeader( 'Location' )[0]; - } - return $url; + return $http_client->headAsync( + $url . '&confirm=' . $confirm, + array( + 'allow_redirects' => false, + 'cookies' => $cookie_jar, + ) + )->then( + static function( $response ) { + return $response->getHeader( 'Location' )[0]; + } + ); + } + ); } /** From 62bb4f7b69200112e73315f0869acf89a998da5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Wed, 20 Jul 2022 01:21:02 +0200 Subject: [PATCH 29/29] Lint fixes --- src/php/frontend/page/class-videos.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/php/frontend/page/class-videos.php b/src/php/frontend/page/class-videos.php index e83be4bb3..7b700e853 100644 --- a/src/php/frontend/page/class-videos.php +++ b/src/php/frontend/page/class-videos.php @@ -40,7 +40,7 @@ public static function videos( $parent_id, $pagination_helper, $options ) { $pagination_helper )->then( static function( $raw_videos ) use ( &$options ) { - $videos = array_map( + $videos = array_map( static function( $video ) use ( &$options ) { return array( 'id' => $video['id'], @@ -63,7 +63,8 @@ static function( $video ) { )->then( static function( $list ) { list( $videos, $video_urls ) = $list; - for( $i = 0; $i < count( $videos ); $i++ ) { + $count = count( $videos ); + for ( $i = 0; $i < $count; $i++ ) { $videos[ $i ]['src'] = $video_urls[ $i ]; } return $videos; @@ -98,7 +99,10 @@ private static function resolve_video_url( $video_id, $mime_type, $size, $web_co } } $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); - return $http_client->getAsync( $web_view_url, array( 'allow_redirects' => false ) )->then( + return $http_client->getAsync( + $web_view_url, + array( 'allow_redirects' => false ) + )->then( static function( $response ) use ( $video_id, $mime_type, $size, $web_content_url ) { if ( 200 === $response->getStatusCode() ) { return self::get_direct_video_url( $web_content_url ); @@ -120,8 +124,9 @@ static function( $response ) use ( $video_id, $mime_type, $size, $web_content_ur private static function get_direct_video_url( $web_content_url ) { $http_client = new \Sgdg\Vendor\GuzzleHttp\Client(); return $http_client->getAsync( $web_content_url, array( 'allow_redirects' => false ) )->then( - static function( $response ) use ( $http_client, $web_content_url ) { + static function( $response ) use ( $http_client, $web_content_url ) { $url = $web_content_url; + // @phan-suppress-next-line PhanPluginNonBoolInLogicalArith if ( ! $response->hasHeader( 'Set-Cookie' ) || 0 !== mb_strpos( $response->getHeader( 'Set-Cookie' )[0], 'download_warning' ) ) { return new \Sgdg\Vendor\GuzzleHttp\Promise\FulfilledPromise( $web_content_url ); }