PluginProbe
wpForo Forum / 2.4.2
wpForo Forum v2.4.2
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / classes / SEO.php

SEO.php in wpForo Forum 2.4.2, at classes/SEO.php

418 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpforo\classes;
4
5 use stdClass;
6
7 // Exit if accessed directly
8 if( ! defined( 'ABSPATH' ) ) exit;
9
10 class SEO {
11 private $options;
12 private $default;
13 private $stylesheet;
14 private $http_protocol = 'HTTP/1.1';
15
16 public function __construct() {
17 $stylesheet_url = preg_replace( '/(^https?:)/', '', WPFORO_URL . '/assets/css/wpforo-sitemap.xsl' );
18 $this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( (string) $stylesheet_url ) . '"?>';
19
20 if( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) {
21 $this->http_protocol = sanitize_text_field( $_SERVER['SERVER_PROTOCOL'] );
22 }
23
24 $this->init();
25 }
26
27 public function init() {
28 $this->init_defaults();
29 $this->init_options();
30 $this->init_hooks();
31 }
32
33 private function init_defaults() {
34 $this->default = new stdClass();
35 $this->default->options = [
36 'sitemap_items_per_page' => 1000,
37 'allow_ping' => 1,
38 'ping_immediately' => 0,
39 ];
40 }
41
42 private function init_options() {
43 $this->options = apply_filters( 'wpforo_seo_options', $this->default->options );
44 }
45
46 private function init_hooks() {
47 add_filter( 'wpforo_before_init_current_object', [ $this, 'redirect' ], 10, 2 );
48 add_action( 'wpforo_ping_search_engines', [ $this, 'ping_search_engines' ] );
49 add_action( 'wpforo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] );
50 add_action( 'wpforo_after_init_classes', function() {
51 if( WPF()->board->get_current( 'is_standalone' ) ) add_action( 'wp_sitemaps_enabled', '__return_false' );
52 }, 1 );
53 }
54
55 public function sitemap_url( $url ) {
56 $date = ( ! empty( $url['lastmod'] ) ? gmdate( 'c', strtotime( $url['lastmod'] . ' GMT' ) ) : null );
57 $url['loc'] = htmlspecialchars( $url['loc'] );
58
59 $output = "\t<url>\n";
60 $output .= "\t\t<loc>" . $this->encode_url_rfc3986( $url['loc'] ) . "</loc>\n";
61 $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date ) . "</lastmod>\n";
62 $output .= "\t</url>\n";
63
64 return apply_filters( 'wpforo_sitemap_url', $output, $url );
65 }
66
67 public function members_sitemap( $paged ) {
68 $paged = absint( $paged );
69 $sitemap = '';
70 if( $paged && $profiles = $this->get_public_profiles( $paged ) ) {
71 foreach( $profiles as $profile ) {
72 if( $profile['loc'] = wpforo_member( $profile['userid'], 'profile_url' ) ) {
73 $sitemap .= $this->sitemap_url( $profile );
74 }
75 }
76 }
77
78 return $sitemap;
79 }
80
81 public function forums_sitemap() {
82 $sitemap = '';
83 if( $forums = $this->get_public_forums() ) {
84 foreach( $forums as $forum ) {
85 $forum['loc'] = wpforo_forum( $forum['forumid'], 'url' );
86 $sitemap .= $this->sitemap_url( $forum );
87 }
88 }
89
90 return $sitemap;
91 }
92
93 public function topics_sitemap( $paged ) {
94 $paged = absint( $paged );
95 $sitemap = '';
96 if( $paged && $topics = $this->get_public_topics( $paged ) ) {
97 foreach( $topics as $topic ) {
98 $topic['loc'] = wpforo_topic( $topic['topicid'], 'url' );
99 $sitemap .= $this->sitemap_url( $topic );
100 }
101 }
102
103 return $sitemap;
104 }
105
106 private function encode_url_rfc3986( $url ) {
107
108 if( filter_var( $url, FILTER_VALIDATE_URL ) ) {
109 return $url;
110 }
111
112 $path = wp_parse_url( $url, PHP_URL_PATH );
113
114 if( ! empty( $path ) && '/' !== $path ) {
115 $encoded_path = explode( '/', $path );
116
117 $encoded_path = array_map( 'rawurldecode', $encoded_path );
118 $encoded_path = array_map( 'rawurlencode', $encoded_path );
119 $encoded_path = implode( '/', $encoded_path );
120 $encoded_path = str_replace( '%7E', '~', $encoded_path ); // PHP <5.3.
121
122 $url = str_replace( $path, $encoded_path, $url );
123 }
124
125 $query = wp_parse_url( $url, PHP_URL_QUERY );
126
127 if( ! empty( $query ) ) {
128
129 parse_str( $query, $parsed_query );
130
131 if( defined( 'PHP_QUERY_RFC3986' ) ) { // PHP 5.4+.
132 $parsed_query = http_build_query( $parsed_query, null, '&amp;', PHP_QUERY_RFC3986 );
133 } else {
134 $parsed_query = http_build_query( $parsed_query, null, '&amp;' );
135 $parsed_query = str_replace( '+', '%20', $parsed_query );
136 $parsed_query = str_replace( '%7E', '~', $parsed_query );
137 }
138
139 $url = str_replace( $query, $parsed_query, $url );
140 }
141
142 return $url;
143 }
144
145 private function get_public_profiles( $paged = 0 ) {
146 $profiles = [];
147 if( $groupids = WPF()->usergroup->get_visible_usergroup_ids() ) {
148 $items_per_page = $this->options['sitemap_items_per_page'];
149 $sql = "SELECT `userid`, FROM_UNIXTIME( IFNULL(`online_time`, UNIX_TIMESTAMP()) ) AS lastmod
150 FROM " . WPF()->tables->profiles . "
151 WHERE `groupid` IN(" . implode( ',', $groupids ) . ")
152 OR `secondary_groupids` REGEXP '(,|^)(" . implode( '|', $groupids ) . ")(,|$)'
153 ORDER BY `userid` ASC";
154
155 if( $paged ) $sql .= " LIMIT " . ( -- $paged * $items_per_page ) . ", $items_per_page";
156 $profiles = (array) WPF()->db->get_results( $sql, ARRAY_A );
157 }
158
159 return $profiles;
160 }
161
162 private function get_public_forums() {
163 $forums = [];
164 if( $forumids = $this->get_public_forumids() ) {
165 $sql = "SELECT `forumid`,
166 CASE
167 WHEN `last_post_date` = '0000-00-00 00:00:00'
168 THEN NOW()
169 ELSE `last_post_date`
170 END AS lastmod
171 FROM " . WPF()->tables->forums . "
172 WHERE `forumid` IN(" . implode( ',', $forumids ) . ")
173 ORDER BY `parentid` ASC, `title` ASC";
174 $forums = (array) WPF()->db->get_results( $sql, ARRAY_A );
175 }
176
177 return $forums;
178 }
179
180 private function get_public_forumids() {
181 $forumids = [];
182 $sql = "SELECT `forumid` FROM " . WPF()->tables->forums . " ORDER BY `parentid` ASC, `title` ASC";
183 if( $_forumids = WPF()->db->get_col( $sql ) ) {
184 foreach( $_forumids as $_forumid ) {
185 if( WPF()->perm->forum_can( 'vf', $_forumid ) ) {
186 $forumids[] = $_forumid;
187 }
188 }
189 }
190
191 return $forumids;
192 }
193
194 private function get_public_topics( $paged = 0 ) {
195 $topics = [];
196 if( $forumids = $this->get_public_forumids() ) {
197 $items_per_page = $this->options['sitemap_items_per_page'];
198 $sql = "SELECT `topicid`, `modified` as lastmod
199 FROM " . WPF()->tables->topics . "
200 WHERE `private` = 0
201 AND `status` = 0
202 AND `forumid` IN(" . implode( ',', $forumids ) . ")
203 ORDER BY `topicid` ASC";
204 if( $paged ) $sql .= " LIMIT " . ( -- $paged * $items_per_page ) . ", $items_per_page";
205 $topics = (array) WPF()->db->get_results( $sql, ARRAY_A );
206 }
207
208 return $topics;
209 }
210
211 private function get_public_profiles_count() {
212 $count = 0;
213 if( $groupids = WPF()->usergroup->get_visible_usergroup_ids() ) {
214 $sql = "SELECT COUNT(*)
215 FROM " . WPF()->tables->profiles . "
216 WHERE `groupid` IN(" . implode( ',', $groupids ) . ")
217 OR `secondary_groupids` REGEXP '(,|^)(" . implode( '|', $groupids ) . ")(,|$)'";
218 $count = (int) WPF()->db->get_var( $sql );
219 }
220
221 return $count;
222 }
223
224 private function get_public_topics_count() {
225 $count = 0;
226 if( $forumids = $this->get_public_forumids() ) {
227 $sql = "SELECT COUNT(*)
228 FROM " . WPF()->tables->topics . "
229 WHERE `private` = 0
230 AND `status` = 0
231 AND `forumid` IN(" . implode( ',', $forumids ) . ")";
232 $count = (int) WPF()->db->get_var( $sql );
233 }
234
235 return $count;
236 }
237
238 public function get_root_map() {
239 $links = [];
240 $items_per_page = $this->options['sitemap_items_per_page'];
241
242 if( wpforo_setting( 'seo', 'topics_sitemap' ) ) {
243 if( $topics_count = $this->get_public_topics_count() ) {
244 $pages_count = ceil( $topics_count / $items_per_page );
245 for( $i = 1; $i <= $pages_count; $i ++ ) {
246 $links[] = trim( wpforo_home_url( 'topic-sitemap' . $i . '.xml' ), '/' );
247 }
248 }
249 }
250
251 if( wpforo_setting( 'seo', 'members_sitemap' ) ) {
252 if( $profiles_count = $this->get_public_profiles_count() ) {
253 $pages_count = ceil( $profiles_count / $items_per_page );
254 for( $i = 1; $i <= $pages_count; $i ++ ) {
255 $links[] = trim( wpforo_home_url( 'profile-sitemap' . $i . '.xml' ), '/' );
256 }
257 }
258 }
259
260 $links[] = trim( wpforo_home_url( 'forum-sitemap.xml' ), '/' );
261
262 // make sitemap index
263 $xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
264
265 foreach( $links as $link ) {
266 $link = htmlspecialchars( $link );
267
268 $xml .= "\t<sitemap>\n";
269 $xml .= "\t\t<loc>" . $link . "</loc>\n";
270 $xml .= "\t</sitemap>\n";
271 }
272
273 $xml .= apply_filters( 'wpforo_sitemap_index', '' );
274 $xml .= '</sitemapindex>';
275
276 return $xml;
277 }
278
279 public function get_sitemap( $type, $paged ) {
280 $transient_key = 'wpforo_seo_sitemap_' . WPF()->board->get_current( 'boardid' ) . $type . $paged;
281
282 if( ! $xml = get_transient( $transient_key ) ) {
283 if( $type ) {
284 $urls = '';
285 switch( $type ) {
286 case 'topic':
287 if( wpforo_setting( 'seo', 'topics_sitemap' ) ) $urls = $this->topics_sitemap( $paged );
288 break;
289 case 'forum':
290 if( wpforo_setting( 'seo', 'forums_sitemap' ) ) $urls = $this->forums_sitemap();
291 break;
292 case 'profile':
293 if( wpforo_setting( 'seo', 'members_sitemap' ) ) $urls = $this->members_sitemap( $paged );
294 break;
295 }
296
297 if( $urls ) {
298 $urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" ' . 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd ' . 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" ' . 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
299 $xml = apply_filters( "wpforo_sitemap_{$type}_urlset", $urlset );
300 $xml .= $urls;
301 $xml .= '</urlset>';
302 }
303 } else {
304 $xml = $this->get_root_map();
305 }
306
307 set_transient( $transient_key, $xml, DAY_IN_SECONDS );
308 }
309
310 return (string) $xml;
311 }
312
313 public function get_output( $sitemap ) {
314 $output = '<?xml version="1.0" encoding="UTF-8"?>';
315
316 if( $this->stylesheet ) {
317 $output .= apply_filters( 'wpforo_stylesheet_url', $this->stylesheet ) . "\n";
318 }
319
320 $output .= $sitemap;
321 $output .= "\n<!-- XML Sitemap generated by wpForo SEO -->";
322
323 return $output;
324 }
325
326 public function output( $sitemap ) {
327
328 if( ! headers_sent() ) {
329 header( $this->http_protocol . ' 200 OK', true, 200 );
330 // Prevent the search engines from indexing the XML Sitemap.
331 header( 'X-Robots-Tag: noindex, follow', true );
332 header( 'Content-Type: text/xml; charset=UTF-8' );
333 }
334
335 echo $this->get_output( $sitemap );
336 }
337
338 public function sitemap_close() {
339 remove_all_actions( 'wp_footer' );
340 die();
341 }
342
343 public function redirect( $current_object, $wpf_url_parse ) {
344 $pattern = '#^[\r\n\t\s\0]*(?<type>\w*)-?sitemap(?:(?<paged>\d*)|_index)\.xml[\r\n\t\s\0]*$#iu';
345 if( ! empty( $wpf_url_parse[0] ) && preg_match( $pattern, basename( (string) $wpf_url_parse[0] ), $match ) ) {
346 $current_object['template'] = 'sitemap';
347
348 $query = [ 'type' => '', 'paged' => 1 ];
349 $query = wpforo_parse_args( $match, $query );
350 $type = trim( (string) $query['type'] );
351 $paged = absint( $query['paged'] );
352 if( ! $paged ) $paged = 1;
353
354 if( $sitemap = $this->get_sitemap( $type, $paged ) ) {
355 $this->output( $sitemap );
356 $this->sitemap_close();
357 } else {
358 $current_object['is_404'] = true;
359 }
360 }
361
362 return $current_object;
363 }
364
365 public function hit_sitemap_index() {
366 wp_remote_get( trim( wpforo_home_url( 'sitemap_index.xml' ), '/' ) );
367 }
368
369 public function ping_search_engines( $url = '' ) {
370 if( ! $this->options['allow_ping'] || ! $this->get_public_forumids() ) return;
371 if( ! $url ) $url = urlencode( trim( wpforo_home_url( 'sitemap_index.xml' ), '/' ) );
372 wp_remote_get( 'https://www.google.com/webmasters/sitemaps/ping?sitemap=' . $url, [ 'blocking' => false ] );
373 wp_remote_get( 'https://www.bing.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
374 }
375
376 public function clear_cache( $type = '', $paged = 1 ) {
377 if( $type == '' && $paged = 1 ) {
378 $like = 'wpforo_seo_sitemap_%';
379 $wheres = [];
380 $wheres[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) );
381 $wheres[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) );
382
383 // Delete transients.
384 $sql = sprintf( 'DELETE FROM %1$s WHERE %2$s', WPF()->db->options, implode( ' OR ', $wheres ) );
385 WPF()->db->query( $sql );
386 } else {
387 $transient_key = 'wpforo_seo_sitemap_' . $type . $paged;
388 delete_transient( $transient_key );
389 }
390
391 switch( $type ) {
392 case 'profile':
393 if( ! wpforo_setting( 'seo', 'members_sitemap' ) ) return;
394 break;
395 case 'forum':
396 if( ! wpforo_setting( 'seo', 'forums_sitemap' ) ) return;
397 break;
398 case 'topic':
399 if( ! wpforo_setting( 'seo', 'topics_sitemap' ) ) return;
400 break;
401 case '':
402 break;
403 default:
404 return;
405 }
406
407 wp_schedule_single_event( ( time() + 300 ), 'wpforo_hit_sitemap_index' );
408
409 if( ! $this->options['allow_ping'] ) return;
410
411 if( $this->options['ping_immediately'] ) {
412 $this->ping_search_engines();
413 } elseif( ! wp_next_scheduled( 'wpforo_ping_search_engines' ) ) {
414 wp_schedule_single_event( ( time() + 300 ), 'wpforo_ping_search_engines' );
415 }
416 }
417 }
418