Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.93% covered (success)
95.93%
118 / 123
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
GitHub
95.93% covered (success)
95.93%
118 / 123
50.00% covered (danger)
50.00%
4 / 8
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update
96.15% covered (success)
96.15%
50 / 52
0.00% covered (danger)
0.00%
0 / 1
8
 get_release
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 activate_licence
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deactivate_licence
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 refresh_licence_details
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_remote_check_update
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 get_remote_product_information
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * GitHub integration.
4 *
5 * If a plugin's header contains a GitHub URI, this class will be used to fetch the latest release from GitHub.
6 *
7 * @package brianhenryie/bh-wp-plugin-updater
8 */
9
10namespace BrianHenryIE\WP_Plugin_Updater\Integrations\GitHub;
11
12use BrianHenryIE\WP_Plugin_Updater\Integrations\GitHub\Model\Release;
13use BrianHenryIE\WP_Plugin_Updater\Integrations\Integration_Interface;
14use BrianHenryIE\WP_Plugin_Updater\Licence;
15use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Headers;
16use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Info;
17use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Info_Interface;
18use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Update_Interface;
19use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Update;
20use BrianHenryIE\WP_Plugin_Updater\Settings_Interface;
21use Github\Client as GitHub_Client;
22use JsonMapper\Handler\FactoryRegistry;
23use JsonMapper\Handler\PropertyMapper;
24use JsonMapper\JsonMapperBuilder;
25use Psr\Http\Client\ClientInterface;
26use Psr\Log\LoggerAwareTrait;
27use Psr\Log\LoggerInterface;
28use Syntatis\WPPluginReadMeParser\Parser as Readme_Parser;
29
30class GitHub implements Integration_Interface {
31    use LoggerAwareTrait;
32
33    protected GitHub_Client $client;
34
35    protected Release $release;
36    protected ?string $changelog_text = null;
37
38    protected ?Readme_Parser $readme          = null;
39    protected ?Plugin_Headers $plugin_headers = null;
40
41
42    public function __construct(
43        ClientInterface $http_client,
44        protected Settings_Interface $settings,
45        LoggerInterface $logger,
46    ) {
47        $this->client = GitHub_Client::createWithHttpClient( $http_client );
48        $this->setLogger( $logger );
49    }
50
51    protected function update(): void {
52
53        if ( 1 !== preg_match( '/github.com\/(?<user>.*?)\/(?<repo>[^\/]*)/', $this->settings->get_licence_server_host(), $output_array ) ) {
54            throw new \Exception( 'Failed to parse GitHub URI user and repo from ' . $this->settings->get_licence_server_host() );
55        }
56
57        $user = $output_array['user'];
58        $repo = $output_array['repo'];
59
60        // TODO: catch not-found exception, no connection etc.
61        /**
62         * @see Client::api()
63         * @see \Github\Api\Repo::releases()
64         * @see \Github\Api\Repository\Releases::all()
65         */
66        $response = $this->client->api( 'repo' )->releases()->all( $user, $repo );
67
68        // TODO: remove '->with' that are not needed.
69        $factory_registry = new FactoryRegistry();
70        $mapper           = JsonMapperBuilder::new()
71                                            ->withDocBlockAnnotationsMiddleware()
72                                            ->withObjectConstructorMiddleware( $factory_registry )
73                                            ->withPropertyMapper( new PropertyMapper( $factory_registry ) )
74                                            ->withTypedPropertiesMiddleware()
75                                            ->withNamespaceResolverMiddleware()
76                                            ->build();
77
78        /** @var Release[] $release_object */
79        $releases = $mapper->mapToClassArrayFromString( json_encode( $response ), Release::class );
80
81        $allow_beta = false;
82
83        if ( $allow_beta ) {
84            $this->release = $releases[0];
85        } else {
86            foreach ( $releases as $release ) {
87                if ( ! $release->is_prerelease() ) {
88                    $this->release = $release;
89                    break;
90                }
91            }
92        }
93
94        // get changelog.md from GitHub at tag commit
95        $changelog_url              = "https://raw.githubusercontent.com/{$user}/{$repo}/{$this->release->get_tag_name()}/CHANGELOG.md";
96        $changelog_request_response = wp_remote_get( $changelog_url );
97        if ( 200 === wp_remote_retrieve_response_code( $changelog_request_response ) ) {
98            $this->changelog_text = $changelog_request_response['body'];
99        }
100
101        // TODO: this is case sensitive.
102        // get readme.txt from GitHub at tag commit
103        $readme_url              = "https://raw.githubusercontent.com/{$user}/{$repo}/{$this->release->get_tag_name()}/README.txt";
104        $readme_request_response = wp_remote_get( $readme_url );
105        if ( 200 === wp_remote_retrieve_response_code( $readme_request_response ) ) {
106            $this->readme = new Readme_Parser( $readme_request_response['body'] );
107        }
108
109        $plugin_file_name             = explode( '/', $this->settings->get_plugin_basename() )[1];
110        $plugin_file_url              = "https://raw.githubusercontent.com/{$user}/{$repo}/{$this->release->get_tag_name()}/{$plugin_file_name}";
111        $plugin_file_request_response = wp_remote_get( $plugin_file_url );
112        if ( 200 === wp_remote_retrieve_response_code( $plugin_file_request_response ) ) {
113
114            /**
115             * TODO: remove unused.
116             *
117             * @see get_plugin_data()
118             */
119            $default_headers = array(
120                'Name'            => 'Plugin Name',
121                'PluginURI'       => 'Plugin URI',
122                'Version'         => 'Version',
123                'Description'     => 'Description',
124                'Author'          => 'Author',
125                'AuthorURI'       => 'Author URI',
126                'TextDomain'      => 'Text Domain',
127                'DomainPath'      => 'Domain Path',
128                'Network'         => 'Network',
129                'RequiresWP'      => 'Requires at least',
130                'RequiresPHP'     => 'Requires PHP',
131                'UpdateURI'       => 'Update URI',
132                'RequiresPlugins' => 'Requires Plugins',
133            );
134
135            // write to tmp dir
136            $tmp_plugin_file_path = get_temp_dir() . $plugin_file_name;
137            file_put_contents( $tmp_plugin_file_path, $plugin_file_request_response['body'] );
138            $this->plugin_headers = new Plugin_Headers( get_file_data( $tmp_plugin_file_path, $default_headers ) );
139            unlink( $tmp_plugin_file_path );
140        }
141    }
142
143    protected function get_release(): Release {
144        if ( ! isset( $this->release ) ) {
145            $this->update();
146        }
147        return $this->release;
148    }
149
150    public function activate_licence( Licence $licence ) {
151        // TODO: Implement activate_licence() method.
152        return new Licence();
153    }
154
155    public function deactivate_licence( Licence $licence ) {
156        // TODO: Implement deactivate_licence() method.
157        return new Licence();
158    }
159
160    public function refresh_licence_details( Licence $licence ): Licence {
161        // TODO: Implement refresh_licence_details() method.
162        return new Licence();
163    }
164
165    public function get_remote_check_update( Licence $licence ): ?Plugin_Update_Interface {
166
167        $release = $this->get_release();
168
169        return new Plugin_Update(
170            id: null,
171            slug: $this->settings->get_plugin_slug(),
172            version: ltrim( $release->get_tag_name(), 'v' ),
173            url: $this->plugin_headers->get_plugin_uri(),
174            package: $release->get_assets()[0]->get_browser_download_url(),
175            tested: $this->readme->tested,
176            requires_php: $this->readme->requires_php ?? $this->plugin_headers->get_requires_php() ?? null,
177            autoupdate: null,
178            icons: null,
179            banners: null,
180            banners_rtl: null,
181            translations: null,
182        );
183    }
184
185    public function get_remote_product_information( Licence $licence ): ?Plugin_Info_Interface {
186
187        $release = $this->get_release();
188
189        return new Plugin_Info(
190            sections: array(),
191            name: $this->plugin_headers->get_name(),
192            slug: $this->settings->get_plugin_slug(),
193            version: ltrim( $release->get_tag_name(), 'v' ),
194            author: $this->plugin_headers->get_author(),
195            author_profile: $this->plugin_headers->get_author_uri(),
196            contributors: array(),
197            requires: $this->plugin_headers->get_requires_wp(),
198            tested: null,
199            requires_php: $this->plugin_headers->get_requires_php(),
200            requires_plugins: $this->plugin_headers->get_requires_plugins(),
201            compatibility: array(),
202            rating: 0,
203            ratings: array(),
204            num_ratings: 0,
205            support_url: 'support_url string',
206            support_threads: 0,
207            support_threads_resolved: 0,
208            active_installs: 0,
209            downloaded: 0,
210            last_updated: $this->release->get_created_at(),
211            added: 'added string',
212            homepage: 'homepage string',
213            short_description: 'short_description string',
214            description: 'description string',
215            download_link: $release->get_assets()[0]->get_browser_download_url(),
216            upgrade_notice: 'upgrade_notice string',
217            screenshots: array(),
218            tags: array(),
219            stable_tag: $this->release->get_tag_name(),
220            versions: array(),
221            business_model: null,
222            repository_url: $this->plugin_headers->get_plugin_uri(),
223            commercial_support_url: 'commercial_support_url string',
224            donate_link: 'donate_link string',
225            banners: array(),
226            icons: array(),
227            blocks: array(),
228            block_assets: array(),
229            author_block_count: 0,
230            author_block_rating: 0,
231            blueprints: array(),
232            preview_link: array(),
233            language_packs: array(),
234            block_translations: array(),
235        );
236    }
237}