PluginProbe
Code Snippets / 3.10.1
Code Snippets v3.10.1
3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 All 64 releases
code-snippets / php / Admin / Menus / Manage / Manage_Menu_Bulk_Download.php

Manage_Menu_Bulk_Download.php in Code Snippets 3.10.1, at php/Admin/Menus/Manage/Manage_Menu_Bulk_Download.php

171 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Admin\Menus\Manage;
4
5 use Code_Snippets\Migration\Export\Download_Code;
6 use Code_Snippets\Model\Snippet;
7 use WP_Error;
8 use function Code_Snippets\code_snippets;
9 use function Code_Snippets\get_snippet;
10
11 /**
12 * Handles bulk snippet code downloads from the manage screen.
13 */
14 class Manage_Menu_Bulk_Download {
15
16 /**
17 * Register hooks for this class.
18 */
19 public function __construct() {
20 add_action( 'admin_init', [ $this, 'handle' ] );
21 }
22
23 /**
24 * Handle a bulk download request.
25 *
26 * @return void
27 */
28 public function handle(): void {
29 if ( ! $this->is_request() ) {
30 return;
31 }
32
33 if ( ! current_user_can( code_snippets()->get_cap() ) ) {
34 $this->send_error( __( 'You are not allowed to download these snippets.', 'code-snippets' ), 403 );
35 }
36
37 $nonce = filter_input( INPUT_POST, 'code_snippets_bulk_download_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
38
39 if ( ! wp_verify_nonce( $nonce, 'code_snippets_bulk_download' ) ) {
40 $this->send_error(
41 __( 'The download request is no longer valid. Please refresh and try again.', 'code-snippets' ),
42 403
43 );
44 }
45
46 $snippets_json = wp_unslash( filter_input( INPUT_POST, 'snippets', FILTER_DEFAULT ) ?? '' );
47 $snippets = $this->resolve_snippets( $snippets_json );
48
49 if ( $snippets instanceof WP_Error ) {
50 $status = $snippets->get_error_data( 'status' );
51 $this->send_error( $snippets->get_error_message(), is_numeric( $status ) ? (int) $status : 403 );
52 }
53
54 if ( empty( $snippets ) ) {
55 $this->send_error( __( 'No snippets were selected for download.', 'code-snippets' ) );
56 }
57
58 $download = 1 === count( $snippets )
59 ? Download_Code::build_snippet_download( $snippets[0] )
60 : Download_Code::build_archive_download( $snippets );
61
62 if ( $download instanceof WP_Error ) {
63 $status = $download->get_error_data( 'status' );
64 $this->send_error( $download->get_error_message(), is_numeric( $status ) ? (int) $status : 500 );
65 }
66
67 $this->send_response( $download );
68 }
69
70 /**
71 * Resolve snippets from a JSON request payload.
72 *
73 * @param string $snippets_json JSON-encoded list of requested snippets.
74 *
75 * @return Snippet[]|WP_Error
76 */
77 public function resolve_snippets( string $snippets_json ) {
78 $payload = '' === $snippets_json ? [] : json_decode( $snippets_json, true );
79
80 if ( ! is_array( $payload ) ) {
81 return [];
82 }
83
84 $snippets = [];
85
86 foreach ( $payload as $snippet_data ) {
87 if ( ! is_array( $snippet_data ) || empty( $snippet_data['id'] ) ) {
88 continue;
89 }
90
91 if ( ! empty( $snippet_data['network'] ) && ! current_user_can( code_snippets()->get_network_cap_name() ) ) {
92 return new WP_Error(
93 'code_snippets_forbidden_network_download',
94 __( 'You are not allowed to download network snippets.', 'code-snippets' ),
95 [ 'status' => 403 ]
96 );
97 }
98
99 $snippet = get_snippet(
100 absint( $snippet_data['id'] ),
101 ! empty( $snippet_data['network'] )
102 );
103
104 if ( $snippet->id ) {
105 $snippets[] = $snippet;
106 }
107 }
108
109 return $snippets;
110 }
111
112 /**
113 * Determine whether the current request is a bulk download request.
114 *
115 * @return bool
116 */
117 private function is_request(): bool {
118 $page = sanitize_key( filter_input( INPUT_GET, 'page' ) ?? '' );
119 $action = sanitize_key( filter_input( INPUT_POST, 'code_snippets_action' ) ?? '' );
120
121 return code_snippets()->get_menu_slug() === $page && 'bulk-download' === $action;
122 }
123
124 /**
125 * Send a download response and end execution.
126 *
127 * @param array{filename:string, content_type:string, content:string} $download Download data.
128 *
129 * @return void
130 */
131 private function send_response( array $download ): void {
132 while ( ob_get_level() ) {
133 ob_end_clean();
134 }
135
136 nocache_headers();
137 send_nosniff_header();
138 header( 'Content-Description: File Transfer' );
139 header( 'Content-Type: ' . $download['content_type'] );
140 header( 'Content-Disposition: attachment; filename="' . $download['filename'] . '"' );
141 header( 'Content-Length: ' . strlen( $download['content'] ) );
142 header( 'X-Suggested-Filename: ' . $download['filename'] );
143
144 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary download payload.
145 echo $download['content'];
146 exit;
147 }
148
149 /**
150 * Send a download error and end execution.
151 *
152 * @param string $message Error message.
153 * @param int $status HTTP status code.
154 *
155 * @return void
156 */
157 private function send_error( string $message, int $status = 400 ): void {
158 while ( ob_get_level() ) {
159 ob_end_clean();
160 }
161
162 status_header( $status );
163 nocache_headers();
164 send_nosniff_header();
165 header( 'Content-Type: text/plain; charset=' . get_option( 'blog_charset' ) );
166
167 echo esc_html( $message );
168 exit;
169 }
170 }
171