1<?php
2/**
3 * Error Protection API: WP_Paused_Extensions_Storage class
4 *
5 * @package WordPress
6 * @since 5.2.0
7 */
8
9/**
10 * Core class used for storing paused extensions.
11 *
12 * @since 5.2.0
13 */
14#[AllowDynamicProperties]
15class WP_Paused_Extensions_Storage {
16
17 /**
18 * Type of extension. Used to key extension storage. Either 'plugin' or 'theme'.
19 *
20 * @since 5.2.0
21 * @var string
22 */
23 protected $type;
24
25 /**
26 * Constructor.
27 *
28 * @since 5.2.0
29 *
30 * @param string $extension_type Extension type. Either 'plugin' or 'theme'.
31 */
32 public function __construct( $extension_type ) {
33 $this->type = $extension_type;
34 }
35
36 /**
37 * Records an extension error.
38 *
39 * Only one error is stored per extension, with subsequent errors for the same extension overriding the
40 * previously stored error.
41 *
42 * @since 5.2.0
43 *
44 * @param string $extension Plugin or theme directory name.
45 * @param array $error {
46 * Error information returned by `error_get_last()`.
47 *
48 * @type int $type The error type.
49 * @type string $file The name of the file in which the error occurred.
50 * @type int $line The line number in which the error occurred.
51 * @type string $message The error message.
52 * }
53 * @return bool True on success, false on failure.
54 */
55 public function set( $extension, $error ) {
56 if ( ! $this->is_api_loaded() ) {
57 return false;
58 }
59
60 $option_name = $this->get_option_name();
61
62 if ( ! $option_name ) {
63 return false;
64 }
65
66 $paused_extensions = (array) get_option( $option_name, array() );
67
68 // Do not update if the error is already stored.
69 if ( isset( $paused_extensions[ $this->type ][ $extension ] ) && $paused_extensions[ $this->type ][ $extension ] === $error ) {
70 return true;
71 }
72
73 $paused_extensions[ $this->type ][ $extension ] = $error;
74
75 return update_option( $option_name, $paused_extensions, false );
76 }
77
78 /**
79 * Forgets a previously recorded extension error.
80 *
81 * @since 5.2.0
82 *
83 * @param string $extension Plugin or theme directory name.
84 * @return bool True on success, false on failure.
85 */
86 public function delete( $extension ) {
87 if ( ! $this->is_api_loaded() ) {
88 return false;
89 }
90
91 $option_name = $this->get_option_name();
92
93 if ( ! $option_name ) {
94 return false;
95 }
96
97 $paused_extensions = (array) get_option( $option_name, array() );
98
99 // Do not delete if no error is stored.
100 if ( ! isset( $paused_extensions[ $this->type ][ $extension ] ) ) {
101 return true;
102 }
103
104 unset( $paused_extensions[ $this->type ][ $extension ] );
105
106 if ( empty( $paused_extensions[ $this->type ] ) ) {
107 unset( $paused_extensions[ $this->type ] );
108 }
109
110 // Clean up the entire option if we're removing the only error.
111 if ( ! $paused_extensions ) {
112 return delete_option( $option_name );
113 }
114
115 return update_option( $option_name, $paused_extensions, false );
116 }
117
118 /**
119 * Gets the error for an extension, if paused.
120 *
121 * @since 5.2.0
122 *
123 * @param string $extension Plugin or theme directory name.
124 * @return array|null Error that is stored, or null if the extension is not paused.
125 */
126 public function get( $extension ) {
127 if ( ! $this->is_api_loaded() ) {
128 return null;
129 }
130
131 $paused_extensions = $this->get_all();
132
133 if ( ! isset( $paused_extensions[ $extension ] ) ) {
134 return null;
135 }
136
137 return $paused_extensions[ $extension ];
138 }
139
140 /**
141 * Gets the paused extensions with their errors.
142 *
143 * @since 5.2.0
144 *
145 * @return array {
146 * Associative array of errors keyed by extension slug.
147 *
148 * @type array ...$0 Error information returned by `error_get_last()`.
149 * }
150 */
151 public function get_all() {
152 if ( ! $this->is_api_loaded() ) {
153 return array();
154 }
155
156 $option_name = $this->get_option_name();
157
158 if ( ! $option_name ) {
159 return array();
160 }
161
162 $paused_extensions = (array) get_option( $option_name, array() );
163
164 return isset( $paused_extensions[ $this->type ] ) ? $paused_extensions[ $this->type ] : array();
165 }
166
167 /**
168 * Remove all paused extensions.
169 *
170 * @since 5.2.0
171 *
172 * @return bool
173 */
174 public function delete_all() {
175 if ( ! $this->is_api_loaded() ) {
176 return false;
177 }
178
179 $option_name = $this->get_option_name();
180
181 if ( ! $option_name ) {
182 return false;
183 }
184
185 $paused_extensions = (array) get_option( $option_name, array() );
186
187 unset( $paused_extensions[ $this->type ] );
188
189 if ( ! $paused_extensions ) {
190 return delete_option( $option_name );
191 }
192
193 return update_option( $option_name, $paused_extensions, false );
194 }
195
196 /**
197 * Checks whether the underlying API to store paused extensions is loaded.
198 *
199 * @since 5.2.0
200 *
201 * @return bool True if the API is loaded, false otherwise.
202 */
203 protected function is_api_loaded() {
204 return function_exists( 'get_option' );
205 }
206
207 /**
208 * Get the option name for storing paused extensions.
209 *
210 * @since 5.2.0
211 *
212 * @return string
213 */
214 protected function get_option_name() {
215 if ( ! wp_recovery_mode()->is_active() ) {
216 return '';
217 }
218
219 $session_id = wp_recovery_mode()->get_session_id();
220 if ( empty( $session_id ) ) {
221 return '';
222 }
223
224 return "{$session_id}_paused_extensions";
225 }
226}
227