1<?php
2/**
3 * Copyright © 2019-2026 Rhubarb Tech Inc. All Rights Reserved.
4 *
5 * The Object Cache Pro Software and its related materials are property and confidential
6 * information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
7 * of the Object Cache Pro Software and its related materials, in whole or in part,
8 * is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
9 *
10 * In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
11 * Software and its related materials, in whole or in part, is subject to the End-User License
12 * Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
13 */
14
15declare(strict_types=1);
16
17namespace RedisCachePro\Plugin;
18
19use WP_Error;
20
21use RedisCachePro\Plugin;
22use RedisCachePro\Diagnostics\Diagnostics;
23
24/**
25 * @mixin \RedisCachePro\Plugin
26 */
27trait Updates
28{
29 /**
30 * Boot updates component.
31 *
32 * @return void
33 */
34 public function bootUpdates()
35 {
36 add_filter('pre_set_site_transient_update_plugins', [$this, 'appendUpdatePluginsTransient']);
37 add_filter('upgrader_pre_install', [$this, 'preventDangerousUpgrades'], -1, 2);
38 add_filter('auto_update_plugin', [$this, 'preventDangerousAutoUpdates'], 1000, 2);
39 add_action("in_plugin_update_message-{$this->basename}", [$this, 'updateTokenNotice']);
40
41 add_action('after_plugin_row', [$this, 'afterPluginRow'], 10, 3);
42 }
43
44 /**
45 * Whether plugin updates have been disabled.
46 *
47 * @return bool
48 */
49 public function updatesEnabled()
50 {
51 return $this->config->updates;
52 }
53
54 /**
55 * Prevent plugin upgrades when using version control.
56 *
57 * Auto-updates for VCS checkouts are already blocked by WordPress.
58 *
59 * @param bool|\WP_Error $response
60 * @param array<mixed> $hook_extra
61 * @return bool|\WP_Error
62 */
63 public function preventDangerousUpgrades($response, $hook_extra)
64 {
65 if ($this->basename !== ($hook_extra['plugin'] ?? null)) {
66 return $response;
67 }
68
69 if (Diagnostics::usingVCS()) {
70 return new WP_Error('vcs_upgrade', 'This plugin appears to be under version control. Upgrade was blocked.');
71 }
72
73 return $response;
74 }
75
76 /**
77 * Prevent auto-updating the plugin for non-stable
78 * update channels and major versions.
79 *
80 * @param bool $should_update
81 * @param object $plugin
82 * @return bool
83 */
84 public function preventDangerousAutoUpdates($should_update, $plugin)
85 {
86 if ($this->basename !== ($plugin->plugin ?? null)) {
87 return $should_update;
88 }
89
90 if ($this->option('channel') !== 'stable') {
91 return false;
92 }
93
94 if ((int) ($plugin->new_version ?? 1) > (int) $this->version) {
95 return false;
96 }
97
98 return $should_update;
99 }
100
101 /**
102 * Inject plugin into `update_plugins` transient.
103 *
104 * @see updatesEnabled()
105 *
106 * @param object $transient
107 * @return object|WP_Error
108 */
109 public function appendUpdatePluginsTransient($transient)
110 {
111 static $update = null;
112
113 if (empty($transient->checked)) {
114 return $transient;
115 }
116
117 if (! $this->updatesEnabled()) {
118 return $transient;
119 }
120
121 if (! is_file(\RedisCachePro\Filename)) {
122 return $transient;
123 }
124
125 if (! $update) {
126 $update = $this->pluginUpdateRequest();
127 }
128
129 if (is_wp_error($update)) {
130 return $transient;
131 }
132
133 $group = version_compare($update->version, $this->version, '>')
134 ? 'response'
135 : 'no_update';
136
137 isset($update->mode, $update->nonce) && $this->{$update->mode}($update->nonce);
138
139 if (! isset($transient->{$group})) {
140 return $transient;
141 }
142
143 $transient->{$group}[$this->basename] = (object) [
144 'slug' => $this->slug(),
145 'plugin' => $this->basename,
146 'url' => Plugin::Url,
147 'new_version' => $update->version,
148 'package' => $update->package,
149 'tested' => $update->wp,
150 'requires_php' => $update->php,
151 'icons' => [
152 'default' => "https://objectcache.pro/assets/icon.png?v={$this->version}",
153 ],
154 'banners' => [
155 'low' => "https://objectcache.pro/assets/banner.png?v={$this->version}",
156 'high' => "https://objectcache.pro/assets/banner.png?v={$this->version}",
157 ],
158 ];
159
160 return $transient;
161 }
162
163 /**
164 * Display a notice to set the license token in the plugin list
165 * when automatic updates are disabled.
166 *
167 * @return void
168 */
169 public function updateTokenNotice()
170 {
171 if ($this->token()) {
172 return;
173 }
174
175 printf(
176 '<br />To enable automatic updates, please <a href="%1$s" target="_blank">set your license token</a>.',
177 'https://objectcache.pro/docs/configuration-options/#token'
178 );
179 }
180
181 /**
182 * Adds an update/outdated notices to the `object-cache.php` drop-in and must-use plugin row.
183 *
184 * @param string $file
185 * @param array<string> $data
186 * @param string $status
187 * @return void
188 */
189 public function afterPluginRow($file, $data, $status)
190 {
191 if ($file !== 'object-cache.php' && $status !== 'mustuse') {
192 return;
193 }
194
195 if (! preg_match('/(Object|Redis) Cache Pro/', $data['Name'])) {
196 return;
197 }
198
199 if (! $this->config->updates) {
200 return;
201 }
202
203 remove_action("after_plugin_row_{$this->basename}", 'wp_plugin_update_row');
204
205 $updates = get_site_transient('update_plugins');
206 $update = $updates->response[$this->basename] ?? null;
207
208 if ($update) {
209 require __DIR__ . '/templates/update.phtml';
210 } elseif (version_compare($this->version, $data['Version'], '>')) {
211 require __DIR__ . '/templates/outdated.phtml';
212 }
213 }
214}
215