Making WordPress.org

source: sites/trunk/wordpress.org/public_html/wp-content/plugins/theme-directory/class-themes-api.php

Last change on this file was 11373, checked in by dd32, 5 months ago

Consistently link to https://profiles.wordpress.org/$user/ with a trailing slash.

See #5810.

  • Property svn:eol-style set to native
File size: 29.4 KB
Line 
1<?php
2
3/**
4 * The WordPress.org Themes API.
5 *
6 * Class Themes_API
7 */
8class Themes_API {
9
10        /**
11         * Array of request parameters.
12         *
13         * @var array
14         */
15        public $request = array();
16
17        /**
18         * Array of parameters for WP_Query.
19         *
20         * @var array
21         */
22        public $query = array();
23
24        /**
25         * Holds the result of a WP_Query query.
26         *
27         * @var array
28         */
29        public $result = array();
30
31        /**
32         * API response.
33         *
34         * @var null|array|object
35         */
36        public $response = null;
37
38        /**
39         * Field defaults, overridden by individual sections.
40         *
41         * @var array
42         */
43        public $fields = array(
44                'description'        => false,
45                'downloaded'         => false,
46                'downloadlink'       => false,
47                'last_updated'       => false,
48                'creation_time'      => false,
49                'parent'             => false,
50                'rating'             => false,
51                'ratings'            => false,
52                'reviews_url'        => false,
53                'screenshot_count'   => false,
54                'screenshot_url'     => true,
55                'screenshots'        => false,
56                'sections'           => false,
57                'tags'               => false,
58                'template'           => false,
59                'versions'           => false,
60                'theme_url'          => false,
61                'extended_author'    => false,
62                'photon_screenshots' => false,
63                'active_installs'    => false,
64                'requires'           => false,
65                'requires_php'       => false,
66                'trac_tickets'       => false,
67        );
68
69        /**
70         * Name of the cache group.
71         *
72         * @var string
73         */
74        private $cache_group = 'theme-info';
75
76        /**
77         * The amount of time to keep information cached.
78         *
79         * @var int
80         */
81        private $cache_life = 600; // 10 minutes.
82
83        /**
84         * Flag the input as having been malformed.
85         *
86         * @var bool
87         */
88        public $bad_input = false;
89
90        /**
91         * Constructor.
92         *
93         * @param string $action
94         * @param array $request
95         */
96        public function __construct( $action = '', $request = array() ) {
97                $this->request = (object) $request;
98
99                // Filter out bad inputs.
100                $scalar_only_fields = [
101                        'author',
102                        'browse',
103                        'user',
104                        'locale',
105                        'per_page',
106                        'slug',
107                        'search',
108                        'theme',
109                        'wp_version',
110                ];
111                foreach ( $scalar_only_fields as $field ) {
112                        if ( isset( $this->request->$field ) && ! is_scalar( $this->request->$field ) ) {
113                                unset( $this->request->$field );
114                                $this->bad_input = true;
115                        }
116                }
117
118                // Favorites requests require a user to fetch favorites for.
119                if ( isset( $this->request->browse ) && 'favorites' === $this->request->browse && ! isset( $this->request->user ) ) {
120                        $this->request->user = '';
121                        $this->bad_input = true;
122                }
123
124                $array_of_string_fields = [
125                        'fields',
126                        'slugs',
127                        'tag',
128                ];
129                foreach ( $array_of_string_fields as $field ) {
130                        if ( isset( $this->request->$field ) ) {
131                                $this->request->$field = $this->array_of_strings( $this->request->$field );
132
133                                // If the resulting field is invalid, ignore it entirely.
134                                if ( ! $this->request->$field ) {
135                                        unset( $this->request->$field );
136                                        $this->bad_input = true;
137                                }
138                        }
139                }
140
141                // The locale we should use is specified by the request
142                add_filter( 'locale', array( $this, 'filter_locale' ) );
143
144                /*
145                 * Supported actions:
146                 * query_themes, theme_information, hot_tags, feature_list.
147                 */
148                $valid_actions = array( 'query_themes', 'theme_information', 'hot_tags', 'feature_list', 'get_commercial_shops' );
149                if ( in_array( $action, $valid_actions, true ) && method_exists( $this, $action ) ) {
150                        $this->$action();
151                } else {
152                        // Assume a friendly wp hacker :)
153                        if ( 'POST' != strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
154                                wp_die( 'Action not implemented. <a href="https://codex.wordpress.org/WordPress.org_API">API Docs</a>' );
155                        } else {
156                                $this->response = (object) array( 'error' => 'Action not implemented' );
157                        }
158                }
159        }
160
161        /**
162         * Filter get_locale() to use the locale which is specified in the request.
163         */
164        function filter_locale( $locale ) {
165                if ( ! empty( $this->request->locale ) ) {
166                        $locale = (string) $this->request->locale;
167                }
168
169                return $locale;
170        }
171
172        /**
173         * Prepares result.
174         *
175         * @return string|void
176         */
177        public function get_result( $format = 'raw' ) {
178                $response = $this->response;
179
180                // Back-compat behaviour for the 1.0/1.1 API's
181                if ( defined( 'THEMES_API_VERSION' ) && THEMES_API_VERSION < 1.2 ) {
182                        if ( isset( $this->response->error ) && 'Theme not found' == $this->response->error ) {
183                                $response = false;
184                        }
185                }
186
187                if ( 'json' === $format ) {
188                        return wp_json_encode( $response );
189                } elseif ( 'php' === $format ) {
190                        return serialize( $response );
191                } elseif ( 'api_object' === $format ) {
192                        return $this;
193                } else { // 'raw' === $format, or anything else.
194                        return $response;
195                }
196        }
197
198        /**
199         * Set the Status header for an API response.
200         */
201        public function set_status_header() {
202                if ( ! empty( $this->bad_input ) ) {
203                        status_header( 400 );
204                } elseif (
205                        isset( $this->response->error ) &&
206                        'Theme not found' == $this->response->error
207                ) {
208                        status_header( 404 );
209                } else {
210                        status_header( 200 );
211                }
212        }
213
214        /* Action functions */
215
216        /**
217         * Gets theme tags, ordered by how popular they are.
218         */
219        public function hot_tags() {
220                $cache_key = sanitize_key( __METHOD__ );
221                if ( false === ( $this->response = wp_cache_get( $cache_key, $this->cache_group ) ) ) {
222                        $tags = get_tags( array(
223                                'orderby'    => 'count',
224                                'order'      => 'DESC',
225                                'hide_empty' => false,
226                        ) );
227
228                        // Format in the API representation.
229                        foreach ( $tags as $tag ) {
230                                $this->response[ $tag->slug ] = array(
231                                        'name'  => $tag->name,
232                                        'slug'  => $tag->slug,
233                                        'count' => $tag->count,
234                                );
235                        }
236
237                        wp_cache_add( $cache_key, $this->response, $this->cache_group, $this->cache_life );
238                }
239
240                if ( ! empty( $this->request->number ) ) {
241                        $this->response = array_slice( $this->response, 0, (int) $this->request->number );
242                }
243        }
244
245        /**
246         * Gets a list of valid "features" aka theme tags.
247         */
248        public function feature_list() {
249                $tags = array(
250                        __( 'Colors' )   => array(
251                                'black'  => __( 'Black' ),
252                                'blue'   => __( 'Blue' ),
253                                'brown'  => __( 'Brown' ),
254                                'gray'   => __( 'Gray' ),
255                                'green'  => __( 'Green' ),
256                                'orange' => __( 'Orange' ),
257                                'pink'   => __( 'Pink' ),
258                                'purple' => __( 'Purple' ),
259                                'red'    => __( 'Red' ),
260                                'silver' => __( 'Silver' ),
261                                'tan'    => __( 'Tan' ),
262                                'white'  => __( 'White' ),
263                                'yellow' => __( 'Yellow' ),
264                                'dark'   => __( 'Dark' ),
265                                'light'  => __( 'Light' ),
266                        ),
267                        __( 'Columns' )  => array(
268                                'one-column'    => __( 'One Column' ),
269                                'two-columns'   => __( 'Two Columns' ),
270                                'three-columns' => __( 'Three Columns' ),
271                                'four-columns'  => __( 'Four Columns' ),
272                                'left-sidebar'  => __( 'Left Sidebar' ),
273                                'right-sidebar' => __( 'Right Sidebar' ),
274                        ),
275                        __( 'Layout' )   => array(
276                                'fixed-layout'      => __( 'Fixed Layout' ),
277                                'fluid-layout'      => __( 'Fluid Layout' ),
278                                'responsive-layout' => __( 'Responsive Layout' ),
279                        ),
280                        __( 'Features' ) => array(
281                                'accessibility-ready'   => __( 'Accessibility Ready' ),
282                                'blavatar'              => __( 'Blavatar' ),
283                                'buddypress'            => __( 'BuddyPress' ),
284                                'custom-background'     => __( 'Custom Background' ),
285                                'custom-colors'         => __( 'Custom Colors' ),
286                                'custom-header'         => __( 'Custom Header' ),
287                                'custom-menu'           => __( 'Custom Menu' ),
288                                'editor-style'          => __( 'Editor Style' ),
289                                'featured-image-header' => __( 'Featured Image Header' ),
290                                'featured-images'       => __( 'Featured Images' ),
291                                'flexible-header'       => __( 'Flexible Header' ),
292                                'front-page-post-form'  => __( 'Front Page Posting' ),
293                                'full-width-template'   => __( 'Full Width Template' ),
294                                'microformats'          => __( 'Microformats' ),
295                                'post-formats'          => __( 'Post Formats' ),
296                                'rtl-language-support'  => __( 'RTL Language Support' ),
297                                'sticky-post'           => __( 'Sticky Post' ),
298                                'theme-options'         => __( 'Theme Options' ),
299                                'threaded-comments'     => __( 'Threaded Comments' ),
300                                'translation-ready'     => __( 'Translation Ready' ),
301                        ),
302                        __( 'Subject' )  => array(
303                                'holiday'       => __( 'Holiday' ),
304                                'photoblogging' => __( 'Photoblogging' ),
305                                'seasonal'      => __( 'Seasonal' ),
306                        )
307                );
308
309                // The 1.2+ api expects a `wp_version` field to be sent and does not use the UA.
310                if ( defined( 'THEMES_API_VERSION' ) && THEMES_API_VERSION >= 1.2 ) {
311                        if ( ! empty( $this->request->wp_version ) ) {
312                                $wp_version = (string) $this->request->wp_version;
313                        }
314                } elseif ( preg_match( '|WordPress/([^;]+)|', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {
315                        // Get version from user agent since it's not explicitly sent to feature_list requests in older API branches.
316                        $wp_version = $matches[1];
317                }
318
319                // Pre 3.8 installs get width tags instead of layout tags.
320                if ( isset( $wp_version ) && version_compare( $wp_version, '3.7.999', '<' ) ) {
321                        unset( $tags[ __( 'Layout' ) ] );
322                        $tags[ __( 'Width' ) ] = array(
323                                'fixed-width'    => __( 'Fixed Width' ),
324                                'flexible-width' => __( 'Flexible Width' ),
325                        );
326
327                        if ( array_key_exists( 'accessibility-ready', $tags[ __( 'Features' ) ] ) ) {
328                                unset( $tags[ __( 'Features' ) ]['accessibility-ready'] );
329                        }
330                }
331
332                if ( ! isset( $wp_version ) || version_compare( $wp_version, '3.9-beta', '>' ) ) {
333                        $tags[ __( 'Layout' ) ] = array_merge( $tags[ __( 'Layout' ) ], $tags[ __( 'Columns' ) ] );
334                        unset( $tags[ __( 'Columns' ) ] );
335                }
336
337                // See https://core.trac.wordpress.org/ticket/33407.
338                if ( ! isset( $wp_version ) || version_compare( $wp_version, '4.6-alpha', '>' ) ) {
339                        unset( $tags[ __( 'Colors' ) ] );
340                        $tags[ __( 'Layout' ) ] = array(
341                                'grid-layout'   => __( 'Grid Layout' ),
342                                'one-column'    => __( 'One Column' ),
343                                'two-columns'   => __( 'Two Columns' ),
344                                'three-columns' => __( 'Three Columns' ),
345                                'four-columns'  => __( 'Four Columns' ),
346                                'left-sidebar'  => __( 'Left Sidebar' ),
347                                'right-sidebar' => __( 'Right Sidebar' ),
348                        );
349
350                        unset( $tags[ __( 'Features' ) ]['blavatar'] );
351                        $tags[ __( 'Features' ) ]['footer-widgets'] = __( 'Footer Widgets' );
352                        $tags[ __( 'Features' ) ]['custom-logo']    = __( 'Custom Logo' );
353                        asort( $tags[ __( 'Features' ) ] ); // To move footer-widgets to the right place.
354
355                        $tags[ __( 'Subject' ) ] = array(
356                                'blog'           => __( 'Blog' ),
357                                'e-commerce'     => __( 'E-Commerce' ),
358                                'education'      => __( 'Education' ),
359                                'entertainment'  => __( 'Entertainment' ),
360                                'food-and-drink' => __( 'Food & Drink' ),
361                                'holiday'        => __( 'Holiday' ),
362                                'news'           => __( 'News' ),
363                                'photography'    => __( 'Photography' ),
364                                'portfolio'      => __( 'Portfolio' ),
365                        );
366                }
367
368                // See https://core.trac.wordpress.org/ticket/46272.
369                if ( ! isset( $wp_version ) || version_compare( $wp_version, '5.2-alpha', '>=' ) ) {
370                        $tags[ __( 'Layout' ) ]['wide-blocks']    = __( 'Wide Blocks' );
371                        $tags[ __( 'Features' ) ]['block-styles'] = __( 'Block Editor Styles' );
372                        asort( $tags[ __( 'Features' ) ] ); // To move block-styles to the right place.
373                }
374
375                // See https://core.trac.wordpress.org/ticket/50164.
376                if ( ! isset( $wp_version ) || version_compare( $wp_version, '5.5-alpha', '>=' ) ) {
377                        $tags[ __( 'Features' ) ]['block-patterns']    = __( 'Block Editor Patterns' );
378                        $tags[ __( 'Features' ) ]['full-site-editing'] = __( 'Full Site Editing' );
379                        asort( $tags[ __( 'Features' ) ] );
380                }
381
382                // See https://core.trac.wordpress.org/ticket/53556.
383                if ( ! isset( $wp_version ) || version_compare( $wp_version, '5.8.1-alpha', '>=' ) ) {
384                        $tags[ __( 'Features' ) ]['template-editing'] = __( 'Template Editing' );
385                        asort( $tags[ __( 'Features' ) ] );
386                }
387
388                // Only return tag slugs, to stay compatible with bbpress-version of Themes API.
389                foreach ( $tags as $title => $group ) {
390                        $tags[ $title ] = array_keys( $group );
391                }
392
393                $this->response = $tags;
394        }
395
396        /**
397         * Retrieve specific information about multiple theme.
398         */
399        public function theme_information_multiple() {
400                if ( empty( $this->request->slugs ) ) {
401                        $this->response = (object) array( 'error' => 'Slugs not provided' );
402                        return;
403                }
404
405                $slugs = (array) $this->request->slugs;
406
407                if ( count( $slugs ) > 100 ) {
408                        $this->response = (object) array( 'error' => 'A maximum of 100 themes can be queried at once.' );
409                        return;
410                }
411
412                $response = array();
413                unset( $this->request->slugs ); // So it doesn't affect caching.
414                foreach ( $slugs as $slug ) {
415                        $this->request->slug = $slug;
416                        $this->theme_information();
417                        $response[ $slug ] = $this->response;
418                }
419
420                $this->response = $response;
421        }
422
423        /**
424         * Retrieve specific information about a theme.
425         */
426        public function theme_information() {
427                global $post;
428
429                // Support the 'slugs' parameter to fetch multiple themes at once.
430                if ( ! empty( $this->request->slugs ) ) {
431                        $this->theme_information_multiple();
432                        return;
433                }
434
435                // Theme slug to identify theme.
436                if ( empty( $this->request->slug ) || ! trim( $this->request->slug ) ) {
437                        $this->response = (object) array( 'error' => 'Slug not provided' );
438                        return;
439                }
440
441                $this->request->slug = trim( $this->request->slug );
442
443                // Set which fields wanted by default:
444                $defaults = array(
445                        'sections'     => true,
446                        'rating'       => true,
447                        'downloaded'   => true,
448                        'downloadlink' => true,
449                        'last_updated' => true,
450                        'homepage'     => true,
451                        'tags'         => true,
452                        'template'     => true,
453                );
454                if ( defined( 'THEMES_API_VERSION' ) && THEMES_API_VERSION >= 1.2 ) {
455                        $defaults['extended_author'] = true;
456                        $defaults['num_ratings'] = true;
457                        $defaults['reviews_url'] = true;
458                        $defaults['parent'] = true;
459                        $defaults['requires'] = true;
460                        $defaults['requires_php'] = true;
461                        $defaults['creation_time'] = true;
462                }
463
464                $this->request->fields = (array) ( $this->request->fields ?? [] );
465
466                $this->fields = array_merge( $this->fields, $defaults, (array) $this->request->fields );
467
468                // If there is a cached result, return that.
469                $cache_key = sanitize_key( __METHOD__ . ':' . get_locale() . ':' . $this->request->slug . ':' . md5( serialize( $this->fields ) ) );
470                if ( false !== ( $this->response = wp_cache_get( $cache_key, $this->cache_group ) ) && empty( $this->request->cache_buster ) ) {
471                        return;
472                }
473
474                if ( !empty( $post ) && 'repopackage' == $post->post_type && $this->request->slug === $post->post_name ) {
475                        $this->response = $this->fill_theme( $post );
476                } else {
477                        // get_post_by_slug()
478                        $themes = get_posts( array(
479                                'name'        => $this->request->slug,
480                                'post_type'   => 'repopackage',
481                                'post_status' => 'publish', // delist will be added by query-modifications.
482                        ) );
483
484                        if ( $themes ) {
485                                $this->response = $this->fill_theme( $themes[0] );
486                        } else {
487                                $this->response = (object) array( 'error' => 'Theme not found' ); // Check get_result() if changing this string.
488                        }
489                }
490
491                wp_cache_set( $cache_key, $this->response, $this->cache_group, $this->cache_life );
492        }
493
494        /**
495         * Get a list of themes.
496         *
497         *  Object:
498         *      info (array)
499         *          page (int)
500         *          pages (int)
501         *          results (int)
502         *      themes (array)
503         *          name
504         *          slug
505         *          version
506         *          author
507         *          rating
508         *          num_ratings
509         *          homepage
510         *          description
511         *          preview_url
512         *          download_url
513         */
514        public function query_themes() {
515                // Set which fields wanted by default:
516                $defaults = array(
517                        'description' => true,
518                        'rating'      => true,
519                        'homepage'    => true,
520                        'template'    => true,
521                );
522                if ( defined( 'THEMES_API_VERSION' ) && THEMES_API_VERSION >= 1.2 ) {
523                        $defaults['extended_author'] = true;
524                        $defaults['num_ratings'] = true;
525                        $defaults['parent'] = true;
526                        $defaults['requires'] = true;
527                        $defaults['requires_php'] = true;
528                }
529
530                $this->request->fields = (array) ( $this->request->fields ?? [] );
531
532                $this->fields = array_merge( $this->fields, $defaults, $this->request->fields );
533
534                // If there is a cached result, return that.
535                $cache_key = sanitize_key( __METHOD__ . ':' . get_locale() . ':' . md5( serialize( $this->request ) . serialize( $this->fields ) ) );
536                if ( false !== ( $this->response = wp_cache_get( $cache_key, $this->cache_group ) ) && empty( $this->request->cache_buster ) ) {
537                        return;
538                }
539
540                $this->result = $this->perform_wp_query();
541
542                // Basic information about the request.
543                $this->response = (object) array(
544                        'info'   => array(),
545                        'themes' => array(),
546                );
547
548                // Basic information about the request.
549                $this->response->info = array(
550                        'page'    => max( 1, $this->result->query_vars['paged'] ),
551                        'pages'   => max( 1, $this->result->max_num_pages ),
552                        'results' => (int) $this->result->found_posts,
553                );
554
555                // Fill up the themes lists.
556                foreach ( (array) $this->result->posts as $theme ) {
557                        $this->response->themes[] = $this->fill_theme( $theme );
558                }
559
560                wp_cache_set( $cache_key, $this->response, $this->cache_group, $this->cache_life );
561        }
562
563        public function perform_wp_query() {
564                $this->query = array(
565                        'post_type'   => 'repopackage',
566                        'post_status' => 'publish',
567                );
568                if ( isset( $this->request->page ) ) {
569                        $this->query['paged'] = (int) $this->request->page;
570                }
571                if ( isset( $this->request->per_page ) ) {
572                        // Maximum of 999 themes per page, and a minimum of 1.
573                        $this->query['posts_per_page'] = min( (int) $this->request->per_page, 999 );
574                        if ( $this->query['posts_per_page'] < 1 ) {
575                                unset( $this->query['posts_per_page'] );
576                        }
577                }
578
579                // Views
580                if ( ! empty( $this->request->browse ) ) {
581                        $this->query['browse'] = (string) $this->request->browse;
582
583                        if ( 'featured' == $this->request->browse ) {
584                                $this->cache_life = HOUR_IN_SECONDS;
585                        } elseif ( 'favorites' == $this->request->browse ) {
586                                $this->query['favorites_user'] = $this->request->user;
587                        }
588
589                }
590
591                // Tags
592                if ( ! empty( $this->request->tag ) ) {
593                        $this->request->tag = (array) $this->request->tag;
594
595                        // Replace updated tags.
596                        $updated_tags = array(
597                                'fixed-width'    => 'fixed-layout',
598                                'flexible-width' => 'fluid-layout',
599                        );
600                        foreach ( $updated_tags as $old => $new ) {
601                                if ( $key = array_search( $old, $this->request->tag ) ) {
602                                        $this->request->tag[ $key ] = $new;
603                                }
604                        }
605
606                        $this->query['tax_query'] = array(
607                                array(
608                                        'taxonomy' => 'post_tag',
609                                        'field'    => 'slug',
610                                        'terms'    => $this->request->tag,
611                                        'operator' => 'AND',
612                                ),
613                        );
614                }
615
616                // Search
617                if ( ! empty( $this->request->search ) ) {
618                        $this->query['s'] = (string) $this->request->search;
619                }
620
621                // Direct theme
622                if ( ! empty( $this->request->theme ) ) {
623                        $this->query['name'] = (string) $this->request->theme;
624
625                        add_filter( 'parse_query', array( $this, 'direct_theme_query' ) );
626                }
627
628                // Author
629                if ( ! empty( $this->request->author ) ) {
630                        $this->query['author_name'] = (string) $this->request->author;
631                }
632
633                // Query
634                return new WP_Query( $this->query );
635        }
636
637        /**
638         * Get a list of commercial theme shops.
639         *
640         *  Object:
641         *      shops (array)
642         *          (object)
643         *              shop (string)
644         *              slug (string)
645         *              haiku (string)
646         *              image (string)
647         *              url (string)
648         */
649        function get_commercial_shops() {
650                if ( false !== ( $this->response = wp_cache_get( 'commercial_theme_shops', $this->cache_group ) ) && empty( $this->request->cache_buster ) ) {
651                        return;
652                }
653
654                $this->response = (object) array(
655                        'shops' => array()
656                );
657
658                $theme_shops = new WP_Query( array(
659                        'post_type'      => 'theme_shop',
660                        'posts_per_page' => -1,
661                        'orderby'        => 'rand(' . gmdate('YmdH') . ')',
662                ) );
663
664                while ( $theme_shops->have_posts() ) {
665                        $theme_shops->the_post();
666
667                        $this->response->shops[] = (object) array(
668                                'shop'  => get_the_title(),
669                                'slug'  => sanitize_title( get_the_title() ),
670                                'haiku' => get_the_content(),
671                                'image' => post_custom( 'image_url' ) ?: sprintf( '//s0.wp.com/mshots/v1/%s?w=572', urlencode( post_custom( 'url' ) ) ),
672                                'url'   => post_custom( 'url' ),
673                        );
674                }
675
676                wp_cache_set( 'commercial_theme_shops', $this->response, $this->cache_group, 15 * 60 );
677        }
678
679        /**
680         * Fill it up with information.
681         *
682         * @param  WP_Theme $theme
683         *
684         * @return object
685         */
686        public function fill_theme( $theme ) {
687                // If there is a cached theme for the current locale, return that.
688                $cache_key = sanitize_key( implode( '-', array( $theme->post_name, md5( serialize( $this->fields ) ), get_locale() ) ) );
689                if ( false !== ( $phil = wp_cache_get( $cache_key, $this->cache_group ) ) && empty( $this->request->cache_buster ) ) {
690                        return $phil;
691                }
692
693                global $wpdb;
694
695                $phil = (object) array(
696                        'name' => $theme->post_title,
697                        'slug' => $theme->post_name,
698                );
699
700                $repo_package  = new WPORG_Themes_Repo_Package( $theme->ID );
701                $phil->version = $repo_package->latest_version();
702
703                $phil->preview_url = "https://wp-themes.com/{$theme->post_name}/";
704
705                $author = get_user_by( 'id', $theme->post_author );
706
707                if ( $this->fields['extended_author'] ) {
708                        $phil->author = (object) array(
709                                // WordPress.org user details.
710                                'user_nicename' => $author->user_nicename,
711                                'profile'       => 'https://profiles.wordpress.org/' . $author->user_nicename . '/',
712                                'avatar'        => 'https://secure.gravatar.com/avatar/' . md5( $author->user_email ) . '?s=96&d=monsterid&r=g',
713                                'display_name'  => $author->display_name ?: $author->user_nicename,
714
715                                // Theme headers details.
716                                'author'        => wporg_themes_get_version_meta( $theme->ID, '_author', $phil->version ),
717                                'author_url'    => wporg_themes_get_version_meta( $theme->ID, '_author_url', $phil->version ),
718                        );
719                } else {
720                        $phil->author = $author->user_nicename;
721                }
722
723                if ( $this->fields['screenshot_url'] || $this->fields['screenshot_count'] || $this->fields['screenshots'] ) {
724
725                        // TODO this whole thing will need refactoring for multiple screenshots, if and when.
726                        $screenshot_base = "https://wp-themes.com/wp-content/themes/{$theme->post_name}/screenshot";
727                        if ( $this->fields['screenshot_url'] ) {
728                                $screenshots = get_post_meta( $theme->ID, '_screenshot', true );
729
730                                if ( $this->fields['photon_screenshots'] ) {
731                                        $phil->screenshot_url = sprintf( 'https://i0.wp.com/themes.svn.wordpress.org/%1$s/%2$s/%3$s', $phil->slug, $phil->version, $screenshots[ $phil->version ] );
732                                } else {
733                                        $phil->screenshot_url = sprintf( '//ts.w.org/wp-content/themes/%1$s/%2$s?ver=%3$s', $phil->slug, $screenshots[ $phil->version ], $phil->version );
734                                }
735                        }
736
737                        if ( $this->fields['screenshot_count'] || $this->fields['screenshots'] ) {
738                                $screenshot_count = 1; // TODO
739                                if ( $screenshot_count < 1 ) {
740                                        $screenshot_count = 1;
741                                }
742
743                                if ( $this->fields['screenshot_count'] ) {
744                                        $phil->screenshot_count = $screenshot_count;
745                                }
746
747                                if ( $this->fields['screenshots'] ) {
748                                        $phil->screenshots = array( $screenshot_base . '.png' );
749                                        for ( $i = 2; $i <= $screenshot_count; $i ++ ) {
750                                                $phil->screenshots[] = $screenshot_count . '-' . $i . '.png';
751                                        }
752                                }
753                        }
754                }
755
756                if ( $this->fields['theme_url'] ) {
757                        $phil->theme_url = wporg_themes_get_version_meta( $theme->ID, '_theme_url', $phil->version );
758                }
759
760                if ( $this->fields['ratings'] ) {
761                        // Amount of reviews for each rating level.
762                        $phil->ratings = \WPORG_Ratings::get_rating_counts( 'theme', $theme->post_name );
763                }
764
765                if ( $this->fields['rating'] ) {
766                        // Return a % rating; Rating range: 0~5.
767                        $phil->rating = \WPORG_Ratings::get_avg_rating( 'theme', $theme->post_name ) * 20;
768                        $phil->num_ratings = \WPORG_Ratings::get_rating_count( 'theme', $theme->post_name );
769                }
770
771                if ( $this->fields['reviews_url'] ) {
772                        $phil->reviews_url = 'https://wordpress.org/support/theme/' . $theme->post_name . '/reviews/';
773                }
774
775                if ( $this->fields['downloaded'] ) {
776                        $key = "theme-down:$theme->post_name";
777
778                        if ( false === ( $phil->downloaded = wp_cache_get( $key, $this->cache_group ) ) ) {
779                                $phil->downloaded = (int) $wpdb->get_var( $wpdb->prepare( "SELECT SUM( downloads ) FROM bb_themes_stats WHERE slug = %s", $theme->post_name ) );
780                                wp_cache_set( $key, $phil->downloaded, $this->cache_group, $this->cache_life );
781                        }
782                }
783
784                if ( $this->fields['active_installs'] ) {
785                        $phil->active_installs = (int) get_post_meta( $theme->ID, '_active_installs', true );
786
787                        // 0, 1m+, rounded to nearest significant digit
788                        if ( $phil->active_installs < 10 ) {
789                                $phil->active_installs = 0;
790                        } elseif ( $phil->active_installs >= 3000000 ) {
791                                $phil->active_installs = 3000000;
792                        } else {
793                                $phil->active_installs = strval( $phil->active_installs )[0] * pow( 10, floor( log10( $phil->active_installs ) ) );
794                        }
795                }
796
797                if ( $this->fields['last_updated'] ) {
798                        $phil->last_updated      = get_post_modified_time( 'Y-m-d', true, $theme->ID, true );
799                        $phil->last_updated_time = get_post_modified_time( 'Y-m-d H:i:s', true, $theme->ID, true );
800                }
801
802                if ( $this->fields['creation_time'] ) {
803                        $phil->creation_time = get_post_time( 'Y-m-d H:i:s', true, $theme->ID, true );
804                }
805
806                if ( $this->fields['homepage'] ) {
807                        $phil->homepage = "https://wordpress.org/themes/{$theme->post_name}/";
808                }
809
810                if ( $this->fields['description'] || $this->fields['sections'] ) {
811                        if ( $this->fields['sections'] ) {
812                                // Client wants Sections.
813                                $phil->sections = array();
814                                if ( preg_match_all( '|--theme-data-(.+?)-->(.*?)<!|ims', $theme->post_content, $pieces ) ) {
815                                        for ( $i = 0; $i < count( $pieces[1] ); $i ++ ) {
816                                                $phil->sections[ $pieces[1][ $i ] ] = trim( $pieces[2][ $i ] );
817                                        }
818                                } else {
819                                        // Doesn't have any sections:
820                                        $phil->sections['description'] = $this->fix_mangled_description( trim( $theme->post_content ) );
821                                }
822                        } else {
823                                // No sections, Ok, Just return the Description (First field?)
824                                if ( strpos( $theme->post_content, '<!--' ) ) {
825                                        $phil->description = trim( substr( $theme->post_content, 0, strpos( $theme->post_content, '<!--' ) ) );
826                                } else {
827                                        $phil->description = trim( $theme->post_content );
828                                }
829                                $phil->description = $this->fix_mangled_description( $phil->description );
830                        }
831                }
832
833                if ( $this->fields['downloadlink'] ) {
834                        $phil->download_link = $this->create_download_link( $theme, $phil->version );
835                }
836
837                if ( $this->fields['tags'] ) {
838                        $phil->tags = array();
839                        foreach ( wp_get_post_tags( $theme->ID ) as $tag ) {
840                                $phil->tags[ $tag->slug ] = $tag->name;
841                        }
842                }
843
844                if ( $theme->post_parent && ( $this->fields['template'] || $this->fields['parent'] ) ) {
845                        $parent = get_post( $theme->post_parent );
846
847                        if ( $parent ) {
848                                if ( $this->fields['template'] ) {
849                                        $phil->template = $parent->post_name;
850                                }
851
852                                if ( $this->fields['parent'] ) {
853                                        $phil->parent = array(
854                                                'slug'     => $parent->post_name,
855                                                'name'     => $parent->post_title,
856                                                'homepage' => "https://wordpress.org/themes/{$parent->post_name}/",
857                                        );
858                                }
859                        }
860                }
861
862                if ( $this->fields['versions'] ) {
863                        $phil->versions = array();
864
865                        foreach ( array_keys( get_post_meta( $theme->ID, '_status', true ) ) as $version ) {
866                                $phil->versions[ $version ] = $this->create_download_link( $theme, $version );
867                        }
868                }
869
870                if ( $this->fields['requires'] ) {
871                        $phil->requires = wporg_themes_get_version_meta( $theme->ID, '_requires', $phil->version );
872                }
873
874                if ( $this->fields['requires_php'] ) {
875                        $phil->requires_php = wporg_themes_get_version_meta( $theme->ID, '_requires_php', $phil->version );
876                }
877
878                if ( $this->fields['trac_tickets'] ) {
879                        $phil->trac_tickets = get_post_meta( $theme->ID, '_ticket_id', true );
880                }
881
882                if ( class_exists( 'GlotPress_Translate_Bridge' ) ) {
883                        $glotpress_project = "wp-themes/{$phil->slug}";
884
885                        $phil->name = GlotPress_Translate_Bridge::translate( $phil->name, $glotpress_project );
886
887                        if ( isset( $phil->description ) ) {
888                                $phil->description = GlotPress_Translate_Bridge::translate( $phil->description, $glotpress_project );
889                        }
890
891                        if ( isset( $phil->sections['description'] ) ) {
892                                $phil->sections['description'] = GlotPress_Translate_Bridge::translate( $phil->sections['description'], $glotpress_project );
893                        }
894
895                }
896
897                wp_cache_set( $cache_key, $phil, $this->cache_group, $this->cache_life );
898
899                return $phil;
900        }
901
902        /* Filter */
903
904        /**
905         * Marks queries for single themes as archive queries.
906         *
907         * When themes are queried directly, namely the `name` parameter is set, WordPress assumes this is a singular view.
908         * If a theme is not published and the user doing the request is not logged in, the query returns empty. In case
909         * the requested theme has a version that is awaiting approval, that would not be a desired outcome.
910         *
911         * @param WP_Query $query
912         *
913         * @return WP_Query
914         */
915        public function direct_theme_query( $query ) {
916                $query->is_single   = false;
917                $query->is_singular = false;
918
919                $query->is_post_type_archive = true;
920                $query->is_archive           = true;
921
922                return $query;
923        }
924
925        /* Helper functions */
926
927        /**
928         * Creates download link.
929         *
930         * @param  WP_Post $theme
931         * @param  string $version
932         *
933         * @return string
934         */
935        private function create_download_link( $theme, $version ) {
936                $url  = 'http://downloads.wordpress.org/theme/';
937                $file = $theme->post_name . '.' . $version . '.zip';
938
939                $file = preg_replace( '/[^a-z0-9_.-]/i', '', $file );
940                $file = preg_replace( '/[.]+/', '.', $file );
941
942                return set_url_scheme( $url . $file );
943        }
944
945        /**
946         * Fixes mangled descriptions.
947         *
948         * @param string $description
949         *
950         * @return string
951         */
952        private function fix_mangled_description( $description ) {
953                $description = str_replace( '&quot;"', '"', $description );
954                $description = str_replace( 'href="//', 'href="http://', $description );
955                $description = strip_tags( $description );
956
957                return $description;
958        }
959
960        /**
961         * Helper method to return an array of trimmed strings.
962         */
963        protected function array_of_strings( $input ) {
964                if ( is_string( $input ) ) {
965                        $input = explode( ',', $input );
966                }
967
968                if ( ! $input || ! is_array( $input ) ) {
969                        return [];
970                }
971
972                foreach ( $input as $k => $v ) {
973                        if ( ! is_scalar( $v ) ) {
974                                unset( $input[ $k ] );
975                        } elseif ( is_string( $v ) ) {
976                                // Don't affect non-strings such as int's and bools.
977                                $input[ $k ] = trim( $v );
978                        }
979                }
980
981                // Only unique if it's a non-associative array.
982                if ( wp_is_numeric_array( $input ) ) {
983                        $input = array_unique( $input );
984                }
985
986                return $input;
987        }
988}
Note: See TracBrowser for help on using the repository browser.