Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 125
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
wpcomsh_register_top_clicks_widget
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
Widget_Top_Clicks
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 8
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 widget
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 form
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 update
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 display_top_clicks
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
110
 get_urls_from_stats
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 shrink_link
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 is_presentable_url
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
110
1<?php // phpcs:ignore Squiz.Commenting.FileComment.Missing
2/**
3 * Top Clicks Widget (retired) from WordPress.com
4 * Copied from: fbhepr%2Skers%2Sgehax%2Sjc%2Qpbagrag%2Szh%2Qcyhtvaf%2Sfgngf.cuc%234198-og
5 */
6class Widget_Top_Clicks extends WP_Widget {
7    /**
8     * Alt option name.
9     *
10     * @var string $alt_option_name
11     */
12    public $alt_option_name = 'widget_stats_topclicks';
13
14    /**
15     * Widget default settings.
16     *
17     * @var array{title:string,count:int,len:int} $defaults
18     */
19    public $defaults = array(
20        'title' => '',
21        'count' => 10,
22        'len'   => 25,
23    );
24
25    /**
26     * Constructor.
27     */
28    public function __construct() {
29        parent::__construct(
30            'top-clicks',
31            __( 'Top Clicks', 'wpcomsh' ),
32            array( 'description' => __( 'List the most-clicked links on your blog.', 'wpcomsh' ) )
33        );
34    }
35
36    /**
37     * Display the widget.
38     *
39     * @param array $args     Widget arguments.
40     * @param array $instance Widget instance.
41     */
42    public function widget( $args, $instance ) {
43        $instance = wp_parse_args( $instance, $this->defaults );
44
45        if ( empty( $instance['title'] ) ) {
46            $instance['title'] = __( 'Top Clicks', 'wpcomsh' );
47        }
48
49        $instance['count'] = isset( $instance['count'] ) ? intval( $instance['count'] ) : null;
50        if ( empty( $instance['count'] ) || $instance['count'] < 1 || 10 < $instance['count'] ) {
51            $instance['count'] = 10;
52        }
53
54        echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
55        echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
56        $this->display_top_clicks( $instance['count'], $instance['len'] );
57        echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
58
59        do_action( 'jetpack_stats_extra', 'widget_view', 'top_clicks' );
60    }
61
62    /**
63     * Display the widget settings form.
64     *
65     * @param array $instance Current settings.
66     * @return never
67     */
68    public function form( $instance ) {
69        $instance = wp_parse_args( $instance, $this->defaults );
70        ?>
71        <p>
72            <label>
73                <?php esc_html_e( 'Title:', 'wpcomsh' ); ?>
74                <input class="widefat" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
75            </label>
76        </p>
77        <p>
78            <label>
79                <?php esc_html_e( 'Display length:', 'wpcomsh' ); ?>
80                <input style="width: 60px;" name="<?php echo esc_attr( $this->get_field_name( 'len' ) ); ?>" type="text" value="<?php echo esc_attr( $instance['len'] ); ?>" />
81            </label>
82        </p>
83        <p>
84            <label>
85                <?php esc_html_e( 'URLs to show:', 'wpcomsh' ); ?>
86                <select name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>">
87                    <?php for ( $i = 1; $i <= 12; ++$i ) { ?>
88                        <option value="<?php echo $i; ?><?php selected( $i, $instance['count'] ); ?>><?php echo $i; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $i is an integer iterator. */ ?></option>
89                    <?php } ?>
90                </select>
91            </label>
92        </p>
93        <p><?php esc_html_e( 'Top Clicks are calculated from 48-72 hours of stats. They take a while to change.', 'wpcomsh' ); ?></p>
94        <?php
95    }
96
97    /**
98     * Update the widget settings.
99     *
100     * @param array $new_instance New settings.
101     * @param array $old_instance Old settings.
102     */
103    public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
104        $new_instance          = wp_parse_args(
105            $new_instance,
106            array(
107                'title' => __( 'Top Clicks', 'wpcomsh' ),
108                'count' => 10,
109                'len'   => 25,
110            )
111        );
112        $new_instance['title'] = wp_strip_all_tags( $new_instance['title'] );
113        $new_instance['count'] = intval( $new_instance['count'] );
114        $new_instance['len']   = intval( $new_instance['len'] );
115
116        if ( $new_instance['len'] < 1 ) {
117            $new_instance['len'] = 20;
118        }
119
120        wp_cache_delete( 'display_top_clicks', 'output' );
121
122        return $new_instance;
123    }
124
125    /**
126     * Display top clicks widget content.
127     *
128     * @param int       $number Number of links to show.
129     * @param int|false $len Trim link text to X chars, or false to not trim.
130     */
131    protected function display_top_clicks( $number, $len = 25 ) {
132        $html = wp_cache_get( 'display_top_clicks', 'output' );
133        if ( empty( $html ) ) {
134            $urls = wp_cache_get( 'display_top_clicks_urls', 'stats' );
135            if ( false === $urls ) {
136                $stats = stats_get_from_restapi( array( 'num' => 3 ), 'clicks' );
137                if ( is_wp_error( $stats ) || empty( $stats ) ) {
138                    $urls = array();
139                } else {
140                    $urls = $this->get_urls_from_stats( $stats );
141                }
142                wp_cache_add( 'display_top_clicks_urls', $urls, 'stats' );
143            }
144
145            $html = '<ul>';
146            if ( ! empty( $urls ) ) {
147                arsort( $urls );
148                foreach ( $urls as $url => $views ) {
149                    if ( ! $number-- ) {
150                        break;
151                    }
152                    if ( strstr( $url, 'pagead2.google' ) ) {
153                        continue;
154                    }
155                    // TEMP: mask out url shorteners to hide Top Clicks spam till we have a better solution
156                    if ( preg_match( '#(?:tinyurl[.]com)/.#', $url ) ) {
157                        continue;
158                    }
159                    $url   = preg_replace( '/http:\/\/wordpress\.redirectingat\.com\/\?id=725X1342&site=[a-zA-Z0-9]+\.WordPress\.com&url=http%3A/', 'http:', $url );
160                    $html .= '<li>' . $this->shrink_link( $url, $len ) . '</li>';
161                }
162            } else {
163                $html .= '<li>' . __( 'None', 'wpcomsh' ) . '</li>';
164            }
165            $html .= '</ul>';
166            $html  = preg_replace( '|<a (.+?)>|', "<a $1 rel='nofollow'>", $html );
167            wp_cache_add( 'display_top_clicks', $html, 'output', 3600 );
168        }
169        echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- as it's HTML that is already escaped in shrink_link().
170    }
171
172    /**
173     * Get click count by URL.
174     *
175     * @param object $stats Stats.
176     *
177     * @return array
178     */
179    protected function get_urls_from_stats( $stats ) {
180        if ( is_wp_error( $stats ) || ! is_object( $stats ) || empty( $stats->days ) ) {
181            return array();
182        }
183        $urls = array();
184        $days = (array) $stats->days;
185        foreach ( $days as $day ) {
186            foreach ( $day->clicks as $click ) {
187                if ( empty( $click->url ) || empty( $click->views ) ) {
188                    continue;
189                }
190                $urls[ $click->url ] = $click->views;
191            }
192        }
193        return $urls;
194    }
195
196    /**
197     * Trim link text.
198     *
199     * @param string    $url Raw URL.
200     * @param int|false $len Trim link text to X chars, or false to not trim.
201     *
202     * @return string
203     */
204    protected function shrink_link( $url, $len = false ) {
205        $text = preg_replace( '!^(mailto:|https?://(www\.)?)!', '', $url );
206        $text = trim( $text, '/' );
207        $text = rawurldecode( $text );
208        if ( $len > 0 && strlen( $text ) > $len ) {
209            $text = wp_html_excerpt( $text, $len ) . '&#8230;';
210        }
211        $text = esc_html( $text );
212
213        $url = esc_attr( $url );
214
215        return "<a href='$url' target='_blank'>$text</a>";
216    }
217
218    /**
219     * Determine if URL is presentable.
220     *
221     * @param string $url URL.
222     *
223     * @return bool
224     */
225    protected function is_presentable_url( $url ) {
226        if ( empty( $url ) ) {
227            return false;
228        }
229
230        // ALL NON-URL REFERRERS ARE ALLOWED UNLESS ADDED HERE
231        if ( in_array(
232            $url,
233            array(
234                'internal',
235                'DOCUMENT_REFERRER',
236            ),
237            true
238        ) ) {
239            return false;
240        }
241        $parts = @ wp_parse_url( $url ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
242        if ( empty( $parts['host'] ) ) {
243            return true;
244        }
245
246        // ALL NON-HTTP REFERRERS FAIL
247        if ( $parts['scheme'] !== 'http' && $parts['scheme'] !== 'https' ) {
248            return false;
249        }
250
251        // ALL HOSTS ARE ALLOWED UNLESS ADDED HERE
252        if ( in_array(
253            $parts['host'],
254            array(
255                'redirect.ad-feeds.com',
256                'shots.snap.com',
257            ),
258            true
259        ) ) {
260            return false;
261        }
262
263        // Filter out wp-admin links
264        if ( str_starts_with( $parts['path'], '/wp-admin/' ) ) {
265            return false;
266        }
267
268        // Filter out clicks on abuse link
269        if ( false !== strpos( $url, 'wordpress.com/abuse' ) ) {
270            return false;
271        }
272
273        if ( wp_check_invalid_utf8( $url ) === '' ) {
274            return false;
275        }
276
277        return true;
278    }
279}
280
281/**
282 * Register the widget.
283 */
284function wpcomsh_register_top_clicks_widget() { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed
285    // Only register the widget if the Jetpack Stats module is active and we already have an existing instance (since the widget is retired).
286    if ( function_exists( 'stats_get_from_restapi' ) && is_active_widget( false, false, 'top-clicks' ) ) {
287        register_widget( 'Widget_Top_Clicks' );
288    }
289}
290add_action( 'widgets_init', 'wpcomsh_register_top_clicks_widget' );