Passed
Push — master ( 6268c8...c33117 )
by Paul
14:11 queued 01:11
created

Updater::getLatestVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 1
c 1
b 1
f 0
dl 0
loc 3
ccs 0
cts 3
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Addons;
4
5
use GeminiLabs\SiteReviews\Helper;
6
use GeminiLabs\SiteReviews\Helpers\Arr;
7
use GeminiLabs\SiteReviews\Helpers\Url;
8
9
class Updater
10
{
11
    /**
12
     * @var string
13
     */
14
    protected $apiUrl;
15
    /**
16
     * @var array
17
     */
18
    protected $data;
19
    /**
20
     * @var bool
21
     */
22
    protected $forceCheck = false;
23
    /**
24
     * @var bool
25
     */
26
    protected $isReady = false;
27
    /**
28
     * @var string
29
     */
30
    protected $plugin;
31
32
    /**
33
     * @param string $apiUrl
34
     * @param string $file
35
     */
36
    public function __construct($apiUrl, $file, array $data = [])
37
    {
38
        if (!file_exists($file)) {
39
            return;
40
        }
41
        if (!function_exists('get_plugin_data')) {
42
            require_once ABSPATH.WPINC.'/plugin.php';
43
        }
44
        $this->apiUrl = trailingslashit(glsr()->filterString('addon/api-url', $apiUrl));
45
        if ($this->apiUrl === Url::home()) {
46
            return;
47
        }
48
        $this->data = wp_parse_args($data, get_plugin_data($file));
49
        $this->plugin = plugin_basename($file);
50
        $this->isReady = true;
51
        if (!glsr()->addon(Arr::get($this->data, 'TextDomain'))) {
52
            $this->forceCheck = true; // don't cache the version details if the addon is not fully active
53
        }
54
    }
55
56
    /**
57
     * @return object
58
     */
59
    public function activateLicense(array $data = [])
60
    {
61
        return $this->request('activate_license', $data);
62
    }
63
64
    /**
65
     * @return object
66
     */
67
    public function checkLicense(array $data = [])
68
    {
69
        return $this->request('check_license', $data);
70
    }
71
72
    /**
73
     * @return object
74
     */
75
    public function deactivateLicense(array $data = [])
76
    {
77
        return $this->request('deactivate_license', $data);
78
    }
79
80
    /**
81
     * @param false|object|array $result
82
     * @param string $action
83
     * @param object $args
84
     * @return mixed
85
     * @filter plugins_api
86
     */
87
    public function filterPluginUpdateDetails($result, $action, $args)
88
    {
89
        if ('plugin_information' != $action
90
            || Arr::get($this->data, 'TextDomain') != Arr::get($args, 'slug')) {
91
            return $result;
92
        }
93
        if ($updateInfo = $this->getPluginUpdate($this->forceCheck)) {
94
            return $this->modifyUpdateDetails($updateInfo);
95
        }
96
        return $result;
97
    }
98
99
    /**
100
     * @param object $transient
101
     * @return object
102
     * @filter pre_set_site_transient_update_plugins
103
     */
104
    public function filterPluginUpdates($transient)
105
    {
106
        if ($updateInfo = $this->getPluginUpdate($this->forceCheck)) {
107
            return $this->modifyPluginUpdates($transient, $updateInfo);
108
        }
109
        return $transient;
110
    }
111
112
    /**
113
     * @return object
114
     */
115
    public function getLatestVersion(array $data = [])
116
    {
117
        return $this->request('get_version', $data);
118
    }
119
120
    /**
121
     * @return void
122
     */
123
    public function init()
124
    {
125
        if ($this->isReady) {
126
            add_filter('plugins_api', [$this, 'filterPluginUpdateDetails'], 10, 3);
127
            add_filter('pre_set_site_transient_update_plugins', [$this, 'filterPluginUpdates'], 999);
128
            add_action('load-update-core.php', [$this, 'onForceUpdateCheck'], 9);
129
            add_action('in_plugin_update_message-'.$this->plugin, [$this, 'renderLicenseMissingLink']);
130
        }
131
    }
132
133
    /**
134
     * @return bool
135
     */
136
    public function isLicenseValid()
137
    {
138
        $result = $this->checkLicense();
139
        return 'valid' === Arr::get($result, 'license');
140
    }
141
142
    /**
143
     * @return void
144
     * @action load-update-core.php
145
     */
146
    public function onForceUpdateCheck()
147
    {
148
        if (!filter_input(INPUT_GET, 'force-check')) {
149
            return;
150
        }
151
        try {
152
            $this->getPluginUpdate(true);
153
        } catch (\Exception $e) {
154
            glsr_log()->error($e->getMessage());
155
        }
156
    }
157
158
    /**
159
     * @return void
160
     * @action in_plugin_update_message-{$this->plugin}
161
     */
162
    public function renderLicenseMissingLink()
163
    {
164
        if (!$this->isLicenseValid()) {
165
            glsr()->render('partials/addons/license-missing');
166
        }
167
    }
168
169
    /**
170
     * @return false|object
171
     */
172
    protected function getCachedVersion()
173
    {
174
        return get_transient($this->getTransientName());
175
    }
176
177
    /**
178
     * @param bool $force
179
     * @return false|object
180
     */
181
    protected function getPluginUpdate($force = false)
182
    {
183
        $version = $this->getCachedVersion();
184
        if (false === $version || false !== $force) {
185
            $version = $this->getLatestVersion();
186
            $this->setCachedVersion($version);
187
        }
188
        if (isset($version->error)) {
189
            glsr_log()->error($version->error);
190
            return false;
191
        }
192
        return $version;
193
    }
194
195
    /**
196
     * @return string
197
     */
198
    protected function getTransientName()
199
    {
200
        return glsr()->prefix.md5(Arr::get($this->data, 'TextDomain'));
201
    }
202
203
    /**
204
     * @param object $transient
205
     * @param object $updateInfo
206
     * @return object
207
     */
208
    protected function modifyPluginUpdates($transient, $updateInfo)
209
    {
210
        $updateInfo->id = glsr()->id.'/'.Arr::get($this->data, 'TextDomain');
211
        $updateInfo->plugin = $this->plugin;
212
        // $updateInfo->requires_php = Arr::get($this->data, 'RequiresPHP');
213
        // $updateInfo->tested = Arr::get($this->data, 'testedTo');
214
        unset($updateInfo->upgrade_notice); // @todo for some reason, this is returned as an array
215
        $transient->checked[$this->plugin] = Arr::get($this->data, 'Version');
216
        $transient->last_checked = time();
217
        if (Helper::isGreaterThan($updateInfo->new_version, Arr::get($this->data, 'Version'))) {
218
            unset($transient->no_update[$this->plugin]);
219
            $updateInfo->update = true;
220
            if (!$this->isLicenseValid()) {
221
                $updateInfo->upgrade_notice = _x('A valid license key is required to download this update.', 'admin-text', 'site-reviews');
222
            }
223
            $transient->response[$this->plugin] = $updateInfo;
224
        } else {
225
            unset($transient->response[$this->plugin]);
226
            $transient->no_update[$this->plugin] = $updateInfo;
227
        }
228
        return $transient;
229
    }
230
231
    /**
232
     * @param object $updateInfo
233
     * @return object
234
     */
235
    protected function modifyUpdateDetails($updateInfo)
236
    {
237
        $updateInfo->author = Arr::get($this->data, 'Author');
238
        $updateInfo->author_profile = Arr::get($this->data, 'AuthorURI');
239
        // $updateInfo->requires = Arr::get($this->data, 'RequiresWP');
240
        // $updateInfo->requires_php = Arr::get($this->data, 'RequiresPHP');
241
        // $updateInfo->tested = Arr::get($this->data, 'testedTo');
242
        $updateInfo->version = $updateInfo->new_version;
243
        unset($updateInfo->contributors); // @todo for some reason, this is not being parsed as an array
244
        return $updateInfo;
245
    }
246
247
    /**
248
     * @param \WP_Error|array $response
249
     * @return object
250
     */
251
    protected function normalizeResponse($response)
252
    {
253
        $body = wp_remote_retrieve_body($response);
254
        if ($data = json_decode($body)) {
255
            $data = array_map('maybe_unserialize', (array) $data);
256
            return (object) $data;
257
        }
258
        $error = is_wp_error($response)
259
            ? $response->get_error_message()
260
            : 'Update server not responding ('.Arr::get($this->data, 'TextDomain').')';
261
        return (object) ['error' => $error];
262
    }
263
264
    /**
265
     * @param string $action activate_license|check_license|deactivate_license|get_version
266
     * @return object
267
     */
268
    protected function request($action, array $data = [])
269
    {
270
        $data = wp_parse_args($data, $this->data);
271
        $response = wp_remote_post($this->apiUrl, [
272
            'body' => [
273
                'edd_action' => $action,
274
                'item_id' => '', // we don't have access to the download ID which is why this is empty
275
                'item_name' => Arr::get($data, 'TextDomain'), // we are using the slug for the name
276
                'license' => Arr::get($data, 'license'),
277
                'slug' => Arr::get($data, 'TextDomain'),
278
                'url' => Url::home(),
279
            ],
280
            'sslverify' => glsr()->filterBool('sslverify/post', false),
281
            'timeout' => 15,
282
        ]);
283
        return $this->normalizeResponse($response);
284
    }
285
286
    /**
287
     * @param object $version
288
     * @return void
289
     */
290
    protected function setCachedVersion($version)
291
    {
292
        if (!isset($version->error)) {
293
            set_transient($this->getTransientName(), $version, 15 * MINUTE_IN_SECONDS);
294
        }
295
    }
296
}
297